dsh-rewind-plugin 0.7.5 → 0.9.0-alpha.1
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.en.md +12 -9
- package/README.md +12 -9
- package/SECURITY.md +6 -7
- package/docs/README.md +2 -1
- package/docs/architecture.md +39 -21
- package/docs/compat/audit.md +32 -34
- package/docs/contract/client-contract.md +1 -1
- package/docs/contract/client-contract.zh.md +1 -1
- package/docs/format.md +11 -8
- package/docs/harness-reference.md +17 -4
- package/docs/release/release.md +82 -24
- package/docs/release/release.zh.md +63 -16
- package/docs/rewind-fix.md +80 -0
- package/docs/rewind-fix.zh.md +80 -0
- package/lib/client.js +15 -41
- package/lib/index.js +531 -73
- package/lib/types/client/hidden.d.ts +26 -35
- package/lib/types/client/index.d.ts +19 -1
- package/lib/types/client/popover.d.ts +8 -9
- package/lib/types/client/portals.d.ts +25 -40
- package/lib/types/client/settings-card.d.ts +3 -4
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/index.d.ts +11 -5
- package/lib/types/locales.d.ts +19 -0
- package/lib/types/rewind-fix.d.ts +109 -0
- package/lib/types/rewind-marker-repair.d.ts +83 -0
- package/lib/types/rewind.d.ts +14 -57
- package/lib/types/session-log-io.d.ts +57 -0
- package/lib/types/snapshot-cleanup.d.ts +3 -3
- package/package.json +44 -41
- package/docs/compat/troubleshooting.md +0 -41
- package/docs/compat/troubleshooting.zh.md +0 -29
- package/lib/types/session-events.d.ts +0 -37
- package/lib/types/settings-locale.d.ts +0 -33
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-rewind/client/hidden
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
9
|
+
/** A chat-snapshot view node as the hiding / composer-refill logic reads it.
|
|
10
|
+
* The harness's view node shape plus the `anchorSeq` the chat snapshot carries
|
|
11
|
+
* (the plugin reads `anchorSeq` off each node; it is not declared on the
|
|
12
|
+
* harness's `ConversationViewNode`). */
|
|
13
|
+
export interface ChatConversationViewNode {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly kind?: string;
|
|
16
|
+
readonly data?: unknown;
|
|
17
|
+
readonly anchorSeq: number;
|
|
18
|
+
}
|
|
9
19
|
/** Minimal chat snapshot reader the hiding logic needs. */
|
|
10
20
|
export interface HiddenChat {
|
|
11
21
|
readonly order: readonly string[];
|
|
@@ -14,55 +24,36 @@ export interface HiddenChat {
|
|
|
14
24
|
};
|
|
15
25
|
}
|
|
16
26
|
/**
|
|
17
|
-
* Reader for one session's live chat snapshot.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* dsh-client-ui-chat through the uiSession slot hook). See
|
|
22
|
-
* SiriLee/dsh-rewind#7.
|
|
27
|
+
* Reader for one session's live chat snapshot. On the 0.1.2-rc.1 line (the
|
|
28
|
+
* plugin's single baseline) the chat is served by the `uiConversation`
|
|
29
|
+
* service's named "chat" view (contributed by dsh-client-ui-chat through the
|
|
30
|
+
* uiSession slot hook).
|
|
23
31
|
*/
|
|
24
32
|
export type ChatOf = (session: {
|
|
25
33
|
readonly sessionId: string;
|
|
26
|
-
getSnapshot(): {
|
|
27
|
-
chat?: unknown;
|
|
28
|
-
};
|
|
29
34
|
} | undefined) => HiddenChat | undefined;
|
|
30
35
|
/**
|
|
31
36
|
* Subscribe to one session's live chat-update signal, for waiting on a chat
|
|
32
|
-
* snapshot change without polling.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `subscribe` is the chat-update signal). `cb` fires whenever the chat
|
|
36
|
-
* snapshot invalidates.
|
|
37
|
+
* snapshot change without polling. The 0.1.2-rc.1 `uiConversation` "chat"
|
|
38
|
+
* view's own `subscribe` is the chat-update signal; `cb` fires whenever the
|
|
39
|
+
* chat snapshot invalidates.
|
|
37
40
|
*/
|
|
38
41
|
export type ChatWatch = (sessionId: string, cb: () => void) => () => void;
|
|
39
42
|
/**
|
|
40
|
-
* Choose the chat-update subscription for `waitForCommand`:
|
|
41
|
-
* `uiConversation` "chat" view's own `subscribe
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* the chat-update signal). Extracted as a pure channel-selection step so the
|
|
45
|
-
* "view vs face" branch is unit-testable; the resolvers are injected by the
|
|
46
|
-
* caller (see `watchChat` in index.ts). Never throws.
|
|
43
|
+
* Choose the chat-update subscription for `waitForCommand`: the
|
|
44
|
+
* `uiConversation` "chat" view's own `subscribe`. Extracted as a pure
|
|
45
|
+
* channel-selection step so the resolver is unit-testable; the resolver is
|
|
46
|
+
* injected by the caller (see `watchChat` in index.ts). Never throws.
|
|
47
47
|
*/
|
|
48
48
|
export declare function resolveChatWatch(resolveView: (sessionId: string) => {
|
|
49
49
|
subscribe?(cb: () => void): () => void;
|
|
50
|
-
} | undefined, resolveFace: (sessionId: string) => {
|
|
51
|
-
subscribe(cb: () => void): () => void;
|
|
52
50
|
} | undefined, sessionId: string, cb: () => void): () => void;
|
|
53
51
|
/**
|
|
54
|
-
* Resolve the chat snapshot
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* view. The view's `getSnapshot()` returns undefined until the named view is
|
|
58
|
-
* registered, so both channels missing degrades to `undefined` (no targets, no
|
|
59
|
-
* hiding — never a crash).
|
|
52
|
+
* Resolve the chat snapshot from the `uiConversation` "chat" view. The view's
|
|
53
|
+
* `getSnapshot()` returns undefined until the named view is registered, which
|
|
54
|
+
* degrades to `undefined` (no targets, no hiding — never a crash).
|
|
60
55
|
*/
|
|
61
|
-
export declare function chatSnapshotOf(
|
|
62
|
-
getSnapshot(): {
|
|
63
|
-
chat?: unknown;
|
|
64
|
-
};
|
|
65
|
-
} | undefined, chatView: {
|
|
56
|
+
export declare function chatSnapshotOf(chatView: {
|
|
66
57
|
getSnapshot(): unknown;
|
|
67
58
|
} | undefined): HiddenChat | undefined;
|
|
68
59
|
/**
|
|
@@ -26,9 +26,27 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @module dsh-rewind/client
|
|
28
28
|
*/
|
|
29
|
-
import type {
|
|
29
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
30
30
|
export declare const name = "dsh-rewind";
|
|
31
31
|
export declare const inject: string[];
|
|
32
|
+
/**
|
|
33
|
+
* The client plugin root context read by `apply(ctx)`. Local structural face:
|
|
34
|
+
* the harness client context is a cordis `Context` augmented by runtime
|
|
35
|
+
* services, so the plugin declares the subset it reads. `sessions` is the real
|
|
36
|
+
* `ISessions` from `@deepseek-ai/dsh-api-session-controller`.
|
|
37
|
+
*/
|
|
38
|
+
export interface ClientContext {
|
|
39
|
+
effect(execute: () => Iterable<unknown>, label?: string): unknown;
|
|
40
|
+
locale: {
|
|
41
|
+
register(namespace: string, messages: Record<string, Record<string, string>>): unknown;
|
|
42
|
+
bind(namespace: string): (key: string) => string;
|
|
43
|
+
subscribe(cb: () => void): () => void;
|
|
44
|
+
};
|
|
45
|
+
sessions: ISessions;
|
|
46
|
+
get(name: string): unknown;
|
|
47
|
+
slots: unknown;
|
|
48
|
+
commandUi: unknown;
|
|
49
|
+
}
|
|
32
50
|
/**
|
|
33
51
|
* Client plugin body: command decoration + parameterized guard + locale + the
|
|
34
52
|
* portal bridge.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module dsh-rewind/client/popover
|
|
15
15
|
*/
|
|
16
|
-
import type { SessionFace } from '@deepseek-ai/dsh-
|
|
17
|
-
import type { CommandNode } from '@deepseek-ai/dsh-client-
|
|
16
|
+
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
17
|
+
import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
18
18
|
import { type ChatOf, type ChatWatch } from './hidden.ts';
|
|
19
19
|
import type { RewindKey } from './locales.ts';
|
|
20
20
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
@@ -33,16 +33,15 @@ export interface PopoverOptions {
|
|
|
33
33
|
readonly onRetract?: () => void;
|
|
34
34
|
readonly preview: string;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Chat reader on the 0.1.2-rc.1 `uiConversation` "chat" view: the durable
|
|
37
|
+
* variant's command probes scan the chat through it. Unused by the
|
|
38
|
+
* pending-retract variant.
|
|
39
39
|
*/
|
|
40
40
|
readonly chatOf: ChatOf;
|
|
41
41
|
/**
|
|
42
42
|
* Subscribe to one session's live chat-update signal, so a probe waiting on
|
|
43
|
-
* a command's chat node can be woken when the chat snapshot changes
|
|
44
|
-
*
|
|
45
|
-
* to `waitForCommand`.
|
|
43
|
+
* a command's chat node can be woken when the chat snapshot changes. Passed
|
|
44
|
+
* straight through to `waitForCommand`.
|
|
46
45
|
*/
|
|
47
46
|
readonly watchChat: ChatWatch;
|
|
48
47
|
/** The button that opened the popover (outside-click ignore target). */
|
|
@@ -70,7 +69,7 @@ export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, m
|
|
|
70
69
|
* session snapshot (command/run + command/done land as one CommandNode).
|
|
71
70
|
* @returns the outcome text-bearing node, or null on timeout.
|
|
72
71
|
*/
|
|
73
|
-
export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs
|
|
72
|
+
export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs: number | undefined, watch: (cb: () => void) => () => void): Promise<{
|
|
74
73
|
kind: 'success' | 'error';
|
|
75
74
|
text?: string;
|
|
76
75
|
} | null>;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @module dsh-rewind/client/portals
|
|
24
24
|
*/
|
|
25
25
|
import { type ReactNode } from 'react';
|
|
26
|
-
import type { SessionFace } from '@deepseek-ai/dsh-
|
|
26
|
+
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
27
27
|
import { type ChatOf, type ChatWatch, type HiddenChat } from './hidden.ts';
|
|
28
28
|
import type { RewindKey } from './locales.ts';
|
|
29
29
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
@@ -53,26 +53,23 @@ export type PortalTarget = {
|
|
|
53
53
|
export interface RewindBridgeDeps {
|
|
54
54
|
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `chatSnapshotOf` in hidden.ts for the channel precedence.
|
|
56
|
+
* Chat reader on the 0.1.2-rc.1 `uiConversation` "chat" view: every chat
|
|
57
|
+
* snapshot read goes through it. See `chatSnapshotOf` in hidden.ts.
|
|
59
58
|
*/
|
|
60
59
|
readonly chatOf: ChatOf;
|
|
61
60
|
/**
|
|
62
61
|
* Subscribe to one session's live chat-update signal, so the composer refill
|
|
63
62
|
* waiting on an executed rewind's chat node can be woken when the chat
|
|
64
|
-
* snapshot changes
|
|
65
|
-
* update). Passed through to `waitForCommand`.
|
|
63
|
+
* snapshot changes. Passed through to `waitForCommand`.
|
|
66
64
|
*/
|
|
67
65
|
readonly watchChat: ChatWatch;
|
|
68
66
|
readonly currentSessionId: () => string | undefined;
|
|
69
67
|
readonly t: Translate;
|
|
70
68
|
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
71
69
|
/**
|
|
72
|
-
* Session-aware
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* the session that just rewound.
|
|
70
|
+
* Session-aware composer writer (see `writeComposer`): the 0.1.2-rc.1
|
|
71
|
+
* `conversation.input` facade `setDraft` when reachable, else the DOM fill.
|
|
72
|
+
* Session-scoped so the refill only lands in the session that just rewound.
|
|
76
73
|
*/
|
|
77
74
|
readonly setComposerText: (sessionId: string, text: string) => boolean;
|
|
78
75
|
}
|
|
@@ -99,20 +96,18 @@ interface ComposerDraftWriter {
|
|
|
99
96
|
setDraft(text: string): void;
|
|
100
97
|
}
|
|
101
98
|
/**
|
|
102
|
-
* Fill the dsh composer with `text
|
|
103
|
-
*
|
|
104
|
-
* `
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* never a throw.
|
|
99
|
+
* Fill the dsh composer with `text` through the 0.1.2-rc.1 `contenteditable`
|
|
100
|
+
* DOM path. Used by `setComposerText` (the harness-facade-aware writer) as the
|
|
101
|
+
* last-resort and by `runRewindAndFill` to put the withdrawn target message
|
|
102
|
+
* back into the composer after a rewind. Best-effort — no composer match means
|
|
103
|
+
* false, never a throw.
|
|
108
104
|
*/
|
|
109
105
|
export declare function fillComposer(text: string): boolean;
|
|
110
106
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* still restores the text. Never throws.
|
|
107
|
+
* Composer write: prefer the harness facade's `setDraft` (0.1.2-rc.1, correct
|
|
108
|
+
* whole-draft replace), then degrade to the DOM `fillComposer` (0.1.2-rc.1
|
|
109
|
+
* contenteditable). A facade that throws (session teardown) is treated as
|
|
110
|
+
* absent so the DOM path still restores the text. Never throws.
|
|
116
111
|
* @param text - the withdrawn target message text.
|
|
117
112
|
* @param facade - the 0.1.2-rc.1 session input draft writer, when reachable.
|
|
118
113
|
* @returns whether a channel applied the text.
|
|
@@ -136,24 +131,15 @@ export declare function runRewindAndFill(session: SessionFace, seq: number, mode
|
|
|
136
131
|
* Locate the actions container of a user/steering seat row — the element the
|
|
137
132
|
* ↶ button portals into (the copy/branch IconActions row).
|
|
138
133
|
*
|
|
139
|
-
*
|
|
140
|
-
* -
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* per-turn tail footer, `TurnTailNodeView`), and the user action row is
|
|
145
|
-
* revealed via CSS `:has()`. The container is instead located structurally:
|
|
146
|
-
* the direct holder of the copy `<button>` (the `MessageIconActions`
|
|
147
|
-
* container, which mounts that button as a direct child —
|
|
148
|
-
* `MessageIconActions.tsx:83,86`).
|
|
149
|
-
*
|
|
150
|
-
* The 0.1.2-alpha.2/3 `[data-actions-reveal]` attribute-marked rows (see
|
|
151
|
-
* `ACTIONS_ROOT_SELECTOR`) are an intermediate state absorbed by the
|
|
152
|
-
* attribute branch below; they are not needed by either endpoint.
|
|
134
|
+
* On the 0.1.2-rc.1 line the `data-time-hover-root` marker lives only on the
|
|
135
|
+
* per-turn tail footer (`TurnTailNodeView`), and the user action row is
|
|
136
|
+
* revealed via CSS `:has()`. The container is located structurally: the direct
|
|
137
|
+
* holder of the copy `<button>` (the `MessageIconActions` container, which
|
|
138
|
+
* mounts that button as a direct child — `MessageIconActions.tsx:83,86`).
|
|
153
139
|
*
|
|
154
140
|
* Returns undefined when no qualifying container is found; the caller refuses
|
|
155
141
|
* to portal (never a crash, never a wrong attachment). Exported as a test seam
|
|
156
|
-
* (see `collectTargets`) so the
|
|
142
|
+
* (see `collectTargets`) so the row-shape finder is exercised directly for
|
|
157
143
|
* both durable and pending row shapes without a full React portal render.
|
|
158
144
|
*/
|
|
159
145
|
export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
|
|
@@ -174,10 +160,9 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
174
160
|
* synchronous full-transcript scan inside a commit microtask.
|
|
175
161
|
*/
|
|
176
162
|
export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
|
|
177
|
-
/** The current composer draft
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* `retractPending`). */
|
|
163
|
+
/** The current composer draft: the 0.1.2-rc.1 contenteditable `textContent`.
|
|
164
|
+
* Empty when the composer is absent. Exported as a test seam (the
|
|
165
|
+
* empty-composer guard in `retractPending`). */
|
|
181
166
|
export declare function composerText(): string;
|
|
182
167
|
/**
|
|
183
168
|
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* It neither imports the client settings typed contract nor depends on the
|
|
14
14
|
* 0.1.2-rc.1-only `mutate` write API: it reads `getSnapshot().value` and writes
|
|
15
|
-
* via the `set(field, value)` method
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* unit-testable in isolation.
|
|
15
|
+
* via the `set(field, value)` method, and the card receives a tiny structural
|
|
16
|
+
* `CleanupCardApi` supplied by `src/client/index.ts` so the component stays
|
|
17
|
+
* harness-agnostic and unit-testable in isolation.
|
|
19
18
|
*
|
|
20
19
|
* @module dsh-rewind/client/settings-card
|
|
21
20
|
*/
|
|
@@ -23,4 +23,4 @@ export declare const CLASS: {
|
|
|
23
23
|
/** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
|
|
24
24
|
export declare const REWIND_ICON_SVG: string;
|
|
25
25
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
26
|
-
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/*
|
|
26
|
+
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Standardized on the 0.1.2 harness card look (border-radius 16px + 0.5px\n border-l4, 0.5px internal separators). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* lives in `src/client/`).
|
|
5
5
|
*
|
|
6
6
|
* Rewind mechanism: planning is pure (`src/rewind.ts`); execution appends a
|
|
7
|
-
* marker `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* marker `user/message` into the session log whose `surfaceOp` replaces every
|
|
8
|
+
* surface node after the target message with the marker. The append-only log
|
|
9
|
+
* (and the rendered transcript) is untouched — only the model-visible surface
|
|
10
|
+
* is cut, so the next request derives its context from the target onward.
|
|
11
|
+
* The marker is an EMPTY `user/message`: v2 reserves surface `replace` to a
|
|
12
|
+
* node that cites every shadowed seq (`sourceEventSeqs`), and
|
|
13
|
+
* `assistant/message` can no longer carry those — so the replacement node is
|
|
14
|
+
* a `user/message`, exactly as /compact's checkpoint is. An empty
|
|
15
|
+
* `user/message` derives to itself (a present-but-empty user turn), so the
|
|
16
|
+
* marker stays as the surface-tail cut point rather than vanishing.
|
|
17
|
+
*
|
|
12
18
|
*
|
|
13
19
|
* File restore (mode `both`) follows Claude Code's checkpointing: the plugin
|
|
14
20
|
* backs up each tracked write-class edit BEFORE it happens (at the
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -69,6 +69,25 @@ export declare const en: {
|
|
|
69
69
|
'cleanup.clearCancelled': string;
|
|
70
70
|
'cleanup.clearFailed': string;
|
|
71
71
|
'cleanup.usage': string;
|
|
72
|
+
'rewindfix.description': string;
|
|
73
|
+
'rewindfix.inputHint': string;
|
|
74
|
+
'rewindfix.dryRun': string;
|
|
75
|
+
'rewindfix.done': string;
|
|
76
|
+
'rewindfix.cancelled': string;
|
|
77
|
+
'rewindfix.launcherGuard': string;
|
|
78
|
+
'rewindfix.counts': string;
|
|
79
|
+
'rewindfix.writeOk': string;
|
|
80
|
+
'rewindfix.skip.loaded': string;
|
|
81
|
+
'rewindfix.skip.noMarkers': string;
|
|
82
|
+
'rewindfix.skip.noArtifact': string;
|
|
83
|
+
'rewindfix.skip.loadedNow': string;
|
|
84
|
+
'rewindfix.fail.unreadable': string;
|
|
85
|
+
'rewindfix.fail.repair': string;
|
|
86
|
+
'rewindfix.fail.locked': string;
|
|
87
|
+
'rewindfix.fail.generic': string;
|
|
88
|
+
'rewindfix.persistenceUnavailable': string;
|
|
89
|
+
'rewindfix.sessionStoreUnavailable': string;
|
|
90
|
+
'rewindfix.locked': string;
|
|
72
91
|
};
|
|
73
92
|
/** The host rewind dictionary key union. */
|
|
74
93
|
export type HostKey = keyof typeof en;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/dsh-rewind-fix` — orchestration layer.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the COMMAND semantics and the per-session repair pipeline. It
|
|
5
|
+
* intentionally does NOT re-implement the physical codec (`./session-log-io.ts`)
|
|
6
|
+
* or the A/B→C transform (`./rewind-marker-repair.ts`); it wires those against
|
|
7
|
+
* the harness's own session services (read path = `sessionPersistence` +
|
|
8
|
+
* `ctx.sessions`; write path = self-contained 2-frame zstd rewrite + atomic
|
|
9
|
+
* tmp+rename, because the persistence layer is append-only & repairing a log
|
|
10
|
+
* must rewrite non-tail events).
|
|
11
|
+
*
|
|
12
|
+
* Safety model (settled in the design):
|
|
13
|
+
* - Only NON-loaded (closed) sessions are repaired. "Loaded" = the harness has
|
|
14
|
+
* the session's in-memory `this.log`, which is the seq authority — rewriting
|
|
15
|
+
* that session's file would fork it.
|
|
16
|
+
* - Per-session `.lock` (O_EXCL) guards against two tabs/processes fixing the
|
|
17
|
+
* same closed session concurrently.
|
|
18
|
+
* - Each repair is backed up and rolled back on write/verify failure; an
|
|
19
|
+
* interrupted run is idempotent (already-C / marker-free sessions are no-ops).
|
|
20
|
+
* - Repaired sessions have their snapshots cleared via `store.clearSession`.
|
|
21
|
+
*
|
|
22
|
+
* There is no streaming progress channel (`CommandResult` returns once), so the
|
|
23
|
+
* command is "dry wait" like `/compact`: the client is told it takes minutes and
|
|
24
|
+
* a single final summary is returned via `command/done`.
|
|
25
|
+
*
|
|
26
|
+
* {@link runRewindFix} is the DOMAIN core: it returns structured data (not a
|
|
27
|
+
* rendered string) over a narrow deps interface, so it is unit-testable without a
|
|
28
|
+
* real cordis `Context` and stays locale-agnostic. `renderRewindFixReport`
|
|
29
|
+
* renders that data through the plugin's locale translator `t`, and
|
|
30
|
+
* `registerRewindFix` adapts a real `Context` + `SnapshotStore` + renderer and
|
|
31
|
+
* registers the command.
|
|
32
|
+
*/
|
|
33
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
34
|
+
import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
|
|
35
|
+
/** A locale renderer: dictionary key + optional `{name}` params → text. */
|
|
36
|
+
export type RenderFn = (key: string, params?: Record<string, string | number>) => string;
|
|
37
|
+
/** Structural view of the snapshot store the repair needs (`clearSession` only). */
|
|
38
|
+
interface SnapshotStoreLike {
|
|
39
|
+
clearSession(sessionId: string): Promise<unknown>;
|
|
40
|
+
}
|
|
41
|
+
/** The narrow dependency set {@link runRewindFix} needs (injected; unit-testable). */
|
|
42
|
+
export interface RewindFixDeps {
|
|
43
|
+
listSnapshots(signal?: AbortSignal): Promise<Array<{
|
|
44
|
+
header: SessionHeader;
|
|
45
|
+
}>>;
|
|
46
|
+
readRaw(id: SessionId, signal?: AbortSignal): Promise<{
|
|
47
|
+
content: string;
|
|
48
|
+
} | undefined>;
|
|
49
|
+
locate(header: SessionHeader): {
|
|
50
|
+
path: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
isSessionLoaded(id: SessionId): boolean;
|
|
53
|
+
clearSession(id: string): Promise<unknown>;
|
|
54
|
+
}
|
|
55
|
+
export interface RewindFixOptions {
|
|
56
|
+
/** `--apply` executes the repair; otherwise dry run (no writes). */
|
|
57
|
+
apply: boolean;
|
|
58
|
+
/** The launcher session itself needs repair (guard warning). */
|
|
59
|
+
launcherHasMarkers: boolean;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
/**
|
|
62
|
+
* Max sessions processed concurrently. Sessions are independent files, so a
|
|
63
|
+
* bounded pool overlaps zstd decode/encode across them (the dominant cost).
|
|
64
|
+
* Default 4; capped by the session count. Higher means more peak memory.
|
|
65
|
+
*/
|
|
66
|
+
concurrency?: number;
|
|
67
|
+
}
|
|
68
|
+
/** Why a session was skipped or failed (drives the localized label). */
|
|
69
|
+
export type OutcomeReason = 'loaded' | 'no-markers' | 'no-artifact' | 'loaded-between' | 'unreadable' | 'repair' | 'locked';
|
|
70
|
+
/** One per-session outcome, carried into the report. */
|
|
71
|
+
export interface SessionOutcome {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly status: 'repaired' | 'skipped' | 'failed';
|
|
74
|
+
readonly a: number;
|
|
75
|
+
readonly b: number;
|
|
76
|
+
readonly c: number;
|
|
77
|
+
readonly reason?: OutcomeReason;
|
|
78
|
+
readonly error?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Structured result of {@link runRewindFix} (locale-agnostic, unit-testable). */
|
|
81
|
+
export interface RewindFixResult {
|
|
82
|
+
readonly launcherHasMarkers: boolean;
|
|
83
|
+
readonly cancelled: boolean;
|
|
84
|
+
readonly scanned: number;
|
|
85
|
+
readonly durationMs: number;
|
|
86
|
+
readonly apply: boolean;
|
|
87
|
+
readonly sessions: SessionOutcome[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Register the `/dsh-rewind-fix` host command. `store` must be the plugin's
|
|
91
|
+
* `SnapshotStore` so repaired sessions get their snapshots cleared; `render` is
|
|
92
|
+
* the plugin's locale translator.
|
|
93
|
+
*/
|
|
94
|
+
export declare function registerRewindFix(ctx: Context, store: SnapshotStoreLike, render: RenderFn): void;
|
|
95
|
+
/**
|
|
96
|
+
* The domain core. Enumerates all persisted sessions, applies the safety rules,
|
|
97
|
+
* repairs each marker-bearing CLOSED session, and returns structured outcome
|
|
98
|
+
* data. It never throws: every per-session failure is recorded and the run
|
|
99
|
+
* continues (idempotent; a rollback restores the original artifact).
|
|
100
|
+
*/
|
|
101
|
+
export declare function runRewindFix(deps: RewindFixDeps, opts: RewindFixOptions): Promise<RewindFixResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Render a {@link RewindFixResult} through the locale translator. The RESULT
|
|
104
|
+
* headline is the FIRST line (so the client card's single-line collapsed
|
|
105
|
+
* summary shows it), followed by the per-session detail lines (shown when the
|
|
106
|
+
* card is expanded).
|
|
107
|
+
*/
|
|
108
|
+
export declare function renderRewindFixReport(result: RewindFixResult, render: RenderFn): string;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure repair core for legacy rewind markers.
|
|
3
|
+
*
|
|
4
|
+
* The `/dsh-rewind-fix` command rewrites the OLD plugin's rewind marker — a
|
|
5
|
+
* bare `assistant/message(turn=N, step=0)` (form A) or a ghost-frame
|
|
6
|
+
* `[step/start][assistant/message][step/end]` inside a closed turn (form B) —
|
|
7
|
+
* into the CURRENT marker shape (form C): an empty-content `user/message` whose
|
|
8
|
+
* `source` cites the plugin and whose `surfaceOp`/`sourceEventSeqs` still carry
|
|
9
|
+
* the surface replace. Form C is what a 0.1.3 harness accepts.
|
|
10
|
+
*
|
|
11
|
+
* This module is PURE: it only recognizes event shapes and rewrites them. It
|
|
12
|
+
* does zero IO and has no dependency on the harness session services. The
|
|
13
|
+
* session-log IO layer (read/write) and the command orchestration live
|
|
14
|
+
* elsewhere; here we export the transform plus the shared C-marker contract so
|
|
15
|
+
* the running plugin and the repair agree on the exact marker data shape.
|
|
16
|
+
*
|
|
17
|
+
* Correctness contract:
|
|
18
|
+
* - A → C is an in-place retype (seq does NOT shift).
|
|
19
|
+
* - B → C deletes the two ghost `step/start`/`step/end` events AND re-denses
|
|
20
|
+
* every surviving event's `seq`, so the whole log is one global compaction —
|
|
21
|
+
* the same mechanism DSH's own compaction/forward-migration uses.
|
|
22
|
+
* - Every reference (surfaceOp/sourceEventSeqs/data.*) is rewritten through a
|
|
23
|
+
* single `oldSeq → newSeq` map, because deleting frames shifts every later
|
|
24
|
+
* event and a later marker's references may cite an earlier marker's seq.
|
|
25
|
+
* - A marker that survives is retargeted to form C and participates in the
|
|
26
|
+
* renumbering, so references INTO it follow the map too.
|
|
27
|
+
*
|
|
28
|
+
* The transform never mutates its inputs: input events are copied and the
|
|
29
|
+
* mutable reference fields are rebuilt onto fresh objects (a decoded event's
|
|
30
|
+
* `data` is deep-frozen, so mutating a range in place would throw).
|
|
31
|
+
*/
|
|
32
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
33
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
34
|
+
/** The rewind-marker source the current plugin writes (form C contract). */
|
|
35
|
+
export declare const REWIND_MARKER_SOURCE: {
|
|
36
|
+
readonly kind: 'plugin';
|
|
37
|
+
readonly plugin: 'dsh-rewind';
|
|
38
|
+
};
|
|
39
|
+
/** The rewind-marker message content: empty (closest to "invisible"). */
|
|
40
|
+
export declare const REWIND_MARKER_CONTENT: readonly ContentBlock[];
|
|
41
|
+
/**
|
|
42
|
+
* Build the form-C marker `data` (the `user/message` payload). When `id` is
|
|
43
|
+
* supplied it is preserved verbatim (the repair keeps the original marker's
|
|
44
|
+
* `message.id`); when omitted a fresh id is generated so the running plugin can
|
|
45
|
+
* reuse this contract for its live `buildMarker()`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildRewindMarkerData(id?: string): Record<string, unknown>;
|
|
48
|
+
/** The surface `replace` op carried by a rewind marker. */
|
|
49
|
+
export interface ReplaceSurfaceOp {
|
|
50
|
+
op: 'replace';
|
|
51
|
+
start: number;
|
|
52
|
+
end: number;
|
|
53
|
+
}
|
|
54
|
+
/** Type guard for the surface `replace` op. */
|
|
55
|
+
export declare function isReplaceSurfaceOp(value: unknown): value is ReplaceSurfaceOp;
|
|
56
|
+
/** Is `event` a legacy rewind marker in either form A or B? */
|
|
57
|
+
export declare function isLegacyRewindMarker(event: Readonly<SessionEvent>): boolean;
|
|
58
|
+
/** Is `event` already a form-C rewind marker? (the target shape) */
|
|
59
|
+
export declare function isFormCMarker(event: Readonly<SessionEvent>): boolean;
|
|
60
|
+
/** Result of {@link repairRewindMarkers}. */
|
|
61
|
+
export interface RepairOutput {
|
|
62
|
+
/** The repaired events, densely seq'd, in log order. */
|
|
63
|
+
events: SessionEvent[];
|
|
64
|
+
/** original event seq → new dense seq for every SURVIVING event. */
|
|
65
|
+
mapSeq: ReadonlyMap<number, number>;
|
|
66
|
+
/** Per-form counts observed in the repaired run. */
|
|
67
|
+
stats: {
|
|
68
|
+
a: number;
|
|
69
|
+
b: number;
|
|
70
|
+
c: number;
|
|
71
|
+
removedGhosts: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Rewrite any number of stacked rewind markers (forms A and B, interleaved)
|
|
76
|
+
* into form C in one global compaction.
|
|
77
|
+
*
|
|
78
|
+
* @param input - the decoded event list, in log order. Not mutated.
|
|
79
|
+
* @returns the repaired event list, the oldSeq→newSeq map, and per-form counts.
|
|
80
|
+
* @throws {Error} when a surviving reference points at a seq that was consumed
|
|
81
|
+
* (a removed ghost frame); failing closed avoids writing a corrupt log.
|
|
82
|
+
*/
|
|
83
|
+
export declare function repairRewindMarkers(input: ReadonlyArray<SessionEvent | unknown>): RepairOutput;
|