@dsh-electron/dsh-plugin-multi-root-workspace 0.1.12 → 0.1.13
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/CHANGELOG.en.md +13 -1
- package/CHANGELOG.md +13 -1
- package/README.en.md +6 -5
- package/README.md +6 -5
- package/cordis.patch.yml +4 -0
- package/lib/client.js +101 -25
- package/lib/client.js.map +1 -1
- package/lib/{command-CmLekG1M.js → command-B9855H7y.js} +1 -1
- package/lib/command.js +1 -1
- package/lib/index.js +2 -2
- package/lib/{protocol-4zdwV7u4.js → protocol-CJOvAvOS.js} +13 -2
- package/lib/{registry-YTUOjmVu.js → registry-Bv431TfO.js} +423 -123
- package/lib/registry.js +2 -2
- package/lib/scope.js +36 -2
- package/lib/types/authority/protocol.d.ts +1 -1
- package/lib/types/authority/recovery.d.ts +58 -0
- package/lib/types/client/RootSwitcher.d.ts +6 -3
- package/lib/types/client/files-tree/adapter.d.ts +63 -1
- package/lib/types/registry.d.ts +98 -0
- package/lib/types/scope.d.ts +18 -0
- package/package.json +1 -1
package/lib/registry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-
|
|
2
|
-
import { o as nativeHostAvailability } from "./protocol-
|
|
1
|
+
import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-Bv431TfO.js";
|
|
2
|
+
import { o as nativeHostAvailability } from "./protocol-CJOvAvOS.js";
|
|
3
3
|
export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec, nativeHostAvailability };
|
package/lib/scope.js
CHANGED
|
@@ -154,6 +154,10 @@ var MultiRootScopeService = class extends Service {
|
|
|
154
154
|
/** Additional roots keyed by canonical primary root; empty until roots are registered. */
|
|
155
155
|
rootsByPrimary = /* @__PURE__ */ new Map();
|
|
156
156
|
listeners = /* @__PURE__ */ new Set();
|
|
157
|
+
/** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
|
|
158
|
+
batchDepth = 0;
|
|
159
|
+
/** True when a batched write coalesced its notification into the flush. */
|
|
160
|
+
batchDirty = false;
|
|
157
161
|
admissionProof;
|
|
158
162
|
/**
|
|
159
163
|
* The registry-authority fact the registry row publishes. Defaults to
|
|
@@ -183,6 +187,36 @@ var MultiRootScopeService = class extends Service {
|
|
|
183
187
|
};
|
|
184
188
|
}
|
|
185
189
|
/**
|
|
190
|
+
* Run `body` as one notification round: `setAdditionalRoots()` and
|
|
191
|
+
* `setRegistryAuthority()` apply their state immediately (a same-value
|
|
192
|
+
* authority write still stays silent), but subscriber invocations coalesce
|
|
193
|
+
* into a single flush when the OUTERMOST batch ends — also when `body`
|
|
194
|
+
* throws, because the state already landed. A write a subscriber makes
|
|
195
|
+
* during the flush is not part of the batch and notifies synchronously
|
|
196
|
+
* under the normal rules.
|
|
197
|
+
* @param body - the writes to run as one round.
|
|
198
|
+
* @returns the body's return value.
|
|
199
|
+
*/
|
|
200
|
+
batch(body) {
|
|
201
|
+
this.batchDepth++;
|
|
202
|
+
try {
|
|
203
|
+
return body();
|
|
204
|
+
} finally {
|
|
205
|
+
if (--this.batchDepth === 0 && this.batchDirty) {
|
|
206
|
+
this.batchDirty = false;
|
|
207
|
+
for (const listener of this.listeners) listener();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Notify subscribers now, or coalesce into the enclosing batch's flush. */
|
|
212
|
+
notifySubscribers() {
|
|
213
|
+
if (this.batchDepth > 0) {
|
|
214
|
+
this.batchDirty = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const listener of this.listeners) listener();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
186
220
|
* The sorted names of the tools `toolAccess` caps — read-only for the
|
|
187
221
|
* prompt contribution. An invalid config is not enumerated (it caps every
|
|
188
222
|
* call but is not a listable tool set).
|
|
@@ -403,7 +437,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
403
437
|
const key = canonicalPath(primaryRoot);
|
|
404
438
|
if (roots.length === 0) this.rootsByPrimary.delete(key);
|
|
405
439
|
else this.rootsByPrimary.set(key, [...roots]);
|
|
406
|
-
|
|
440
|
+
this.notifySubscribers();
|
|
407
441
|
}
|
|
408
442
|
/**
|
|
409
443
|
* Publish the registry-authority fact. This is the REGISTRY's write port —
|
|
@@ -420,7 +454,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
420
454
|
const next = available ? "available" : "unavailable";
|
|
421
455
|
if (this.registryAuthority === next) return;
|
|
422
456
|
this.registryAuthority = next;
|
|
423
|
-
|
|
457
|
+
this.notifySubscribers();
|
|
424
458
|
}
|
|
425
459
|
};
|
|
426
460
|
//#endregion
|
|
@@ -46,7 +46,7 @@ export declare class AuthorityClient {
|
|
|
46
46
|
private closed;
|
|
47
47
|
epoch: string | undefined;
|
|
48
48
|
private constructor();
|
|
49
|
-
static connect(endpoint: string, store: string, limit: number, onChange: (snapshot: Snapshot) => void, disconnected: () => void): Promise<AuthorityClient>;
|
|
49
|
+
static connect(endpoint: string, store: string, limit: number, onChange: (snapshot: Snapshot) => void, disconnected: () => void, signal?: AbortSignal): Promise<AuthorityClient>;
|
|
50
50
|
request(method: Method, args: readonly unknown[]): Promise<unknown>;
|
|
51
51
|
close(): void;
|
|
52
52
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded background retry for the registry authority: one pending timer and
|
|
3
|
+
* one in-flight attempt per instance, post-completion backoff, and local
|
|
4
|
+
* cancellation on dispose. The scheduler owns NO policy — the caller supplies
|
|
5
|
+
* `run` (one recovery attempt) and `eligible` (whether a degraded state still
|
|
6
|
+
* wants recovery), so the same object can drive contended, follower
|
|
7
|
+
* resync, and retryable storage failures.
|
|
8
|
+
*
|
|
9
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/authority/recovery
|
|
10
|
+
*/
|
|
11
|
+
/** Injectable clock so fake-timer specs can drive the scheduler exactly. */
|
|
12
|
+
export interface RecoveryTimers {
|
|
13
|
+
setTimeout: typeof setTimeout;
|
|
14
|
+
clearTimeout: typeof clearTimeout;
|
|
15
|
+
random(): number;
|
|
16
|
+
now(): number;
|
|
17
|
+
}
|
|
18
|
+
/** What the scheduler is doing, for diagnostics surfaces. */
|
|
19
|
+
export type RecoveryPhase = 'idle' | 'waiting' | 'attempting' | 'disabled';
|
|
20
|
+
export declare class RecoveryScheduler {
|
|
21
|
+
private readonly run;
|
|
22
|
+
private readonly eligible;
|
|
23
|
+
private readonly enabled;
|
|
24
|
+
private readonly timers;
|
|
25
|
+
private timer;
|
|
26
|
+
private inFlight;
|
|
27
|
+
private disposed;
|
|
28
|
+
private attempts;
|
|
29
|
+
private retryAt;
|
|
30
|
+
constructor(options: {
|
|
31
|
+
run: () => Promise<void>;
|
|
32
|
+
eligible: () => boolean;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
timers?: RecoveryTimers;
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Ask for a recovery attempt. Called after every `ensureAuthority()` and on
|
|
38
|
+
* every downgrade event. A healthy state (`!eligible()`) resets the backoff;
|
|
39
|
+
* a pending timer or in-flight attempt is never duplicated or reset.
|
|
40
|
+
*/
|
|
41
|
+
arm(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Progress event (a follower connection was just adopted): the backoff is
|
|
44
|
+
* earned, so reset it and run the next attempt immediately — still through
|
|
45
|
+
* the single pending timer.
|
|
46
|
+
*/
|
|
47
|
+
kick(): void;
|
|
48
|
+
/** Stop scheduling. An in-flight attempt is left to finish on its own. */
|
|
49
|
+
dispose(): void;
|
|
50
|
+
get state(): {
|
|
51
|
+
phase: RecoveryPhase;
|
|
52
|
+
attempts: number;
|
|
53
|
+
nextRetryAt: number | undefined;
|
|
54
|
+
};
|
|
55
|
+
private clearTimer;
|
|
56
|
+
private delay;
|
|
57
|
+
private tick;
|
|
58
|
+
}
|
|
@@ -32,9 +32,12 @@
|
|
|
32
32
|
* the same element's `closest('[data-dockkit-content]')` — the dockkit
|
|
33
33
|
* tabHost section — with a generated fallback where the hook is absent.
|
|
34
34
|
*
|
|
35
|
-
* The selected root
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* The selected root lives in the inject product's session-scoped snapshot,
|
|
36
|
+
* keyed by that dockkit tab id — component state could not survive the
|
|
37
|
+
* remount every file open causes (the opened tab takes the pane, the Files
|
|
38
|
+
* body unmounts with the whole occupant subtree). One selection per tab;
|
|
39
|
+
* session switches mint a fresh inject product, so no selection crosses
|
|
40
|
+
* sessions, and a registry eviction reconciles it back to primary.
|
|
38
41
|
*
|
|
39
42
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/client/RootSwitcher
|
|
40
43
|
*/
|
|
@@ -25,19 +25,72 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
27
27
|
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client';
|
|
28
|
-
import type
|
|
28
|
+
import { type BoundActions, type SnapshotSelectorHook } from '@deepseek-ai/dsh-client-store';
|
|
29
|
+
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
|
|
30
|
+
import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
|
|
29
31
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
30
32
|
import { type FilesInjected } from './face.ts';
|
|
31
33
|
import { createFilesStore } from './store.ts';
|
|
32
34
|
import { filesTreeSurfaceSupported } from './primitives.ts';
|
|
33
35
|
import type { FilesTreeProps } from './FilesTree.tsx';
|
|
36
|
+
import type { RootView } from '../../contract.ts';
|
|
34
37
|
import { NS } from './locales.ts';
|
|
38
|
+
/**
|
|
39
|
+
* One session's per-tab additional-root selections, keyed by the pane's
|
|
40
|
+
* dockkit tab id. An absent entry means the primary root.
|
|
41
|
+
*/
|
|
42
|
+
export type TreeSelection = Readonly<Record<string, RootView>>;
|
|
43
|
+
/**
|
|
44
|
+
* The lifetimes a selection runs under, keyed by tab like the store bucket:
|
|
45
|
+
* while an entry's signal is live the tree keeps its loaded levels, watches,
|
|
46
|
+
* expansion, and scroll — exactly what upstream's tab-record signal gives the
|
|
47
|
+
* native tree across a body remount (opening a file deactivates this tab,
|
|
48
|
+
* unmounting its body without ending the record).
|
|
49
|
+
*/
|
|
50
|
+
export interface FilesTreeTakeover {
|
|
51
|
+
/**
|
|
52
|
+
* The live signal for (tabId, root); reuses the live entry so a remounted
|
|
53
|
+
* body finds its bucket and watches intact. A different root — or an entry
|
|
54
|
+
* already aborted — ends the previous controller before minting the next.
|
|
55
|
+
* @param tabId - the pane's dockkit tab id.
|
|
56
|
+
* @param root - absolute path of the root being browsed.
|
|
57
|
+
* @param section - the dockkit tabHost section the caller mounted under; a
|
|
58
|
+
* live entry keeps an observer on it that ends the takeover whenever the
|
|
59
|
+
* section leaves the document — the record's real end — including when the
|
|
60
|
+
* body that mounted it has long been unmounted itself.
|
|
61
|
+
* @returns the signal the forked tree lives under.
|
|
62
|
+
*/
|
|
63
|
+
readonly signal: (tabId: TabId, root: string, section?: Element) => AbortSignal;
|
|
64
|
+
/**
|
|
65
|
+
* Abort and drop one tab's takeover: the reader deselected or switched
|
|
66
|
+
* roots, or the tab record itself went away.
|
|
67
|
+
* @param tabId - the pane's dockkit tab id.
|
|
68
|
+
*/
|
|
69
|
+
readonly end: (tabId: TabId) => void;
|
|
70
|
+
}
|
|
35
71
|
/** The inject face the registration composes onto the occupant's props. */
|
|
36
72
|
export interface FilesTreeInjected {
|
|
37
73
|
/** The verbatim files face (list/watch bookkeeping included). */
|
|
38
74
|
readonly face: FilesInjected;
|
|
39
75
|
/** Open one `dsh-resource://file` address in the host viewer. */
|
|
40
76
|
readonly openFile: (address: string) => void;
|
|
77
|
+
/**
|
|
78
|
+
* The reserved inject compartment: the slot renderer binds each source to a
|
|
79
|
+
* `use<Name>` selector hook, so `treeSelection` reaches the component as
|
|
80
|
+
* `useTreeSelection`, never as a prop under its own key.
|
|
81
|
+
*/
|
|
82
|
+
readonly hooks: {
|
|
83
|
+
readonly treeSelection: HostObservable<TreeSelection>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Persist one pane's selected root; `'primary'` drops its entry. The
|
|
87
|
+
* session-scoped snapshot survives the body remount a file open causes.
|
|
88
|
+
* @param tabId - the pane's dockkit tab id.
|
|
89
|
+
* @param root - the selected root, or `'primary'` to browse the session root.
|
|
90
|
+
*/
|
|
91
|
+
readonly selectRoot: (tabId: TabId, root: RootView | 'primary') => void;
|
|
92
|
+
/** The forked tree's lifetimes; see {@link FilesTreeTakeover}. */
|
|
93
|
+
readonly takeover: FilesTreeTakeover;
|
|
41
94
|
}
|
|
42
95
|
/** Everything `FilesTree` needs that the slot framework does not synthesize. */
|
|
43
96
|
export interface FilesTreeSurface {
|
|
@@ -47,6 +100,12 @@ export interface FilesTreeSurface {
|
|
|
47
100
|
readonly face: FilesInjected;
|
|
48
101
|
readonly openFile: (address: string) => void;
|
|
49
102
|
readonly t: FilesTreeProps['t'];
|
|
103
|
+
/** Selector hook over this session's per-tab root selections. */
|
|
104
|
+
readonly useTreeSelection: SnapshotSelectorHook<TreeSelection>;
|
|
105
|
+
/** The selection's write side; see {@link FilesTreeInjected.selectRoot}. */
|
|
106
|
+
readonly selectRoot: FilesTreeInjected['selectRoot'];
|
|
107
|
+
/** The forked tree's lifetimes; see {@link FilesTreeTakeover}. */
|
|
108
|
+
readonly takeover: FilesTreeTakeover;
|
|
50
109
|
}
|
|
51
110
|
/**
|
|
52
111
|
* A `ClientRemote` stand-in that resolves the real service per call.
|
|
@@ -89,6 +148,9 @@ export declare function createFilesTreeSurface(props: {
|
|
|
89
148
|
readonly actions: FilesTreeSurface['actions'];
|
|
90
149
|
readonly face: FilesInjected | undefined;
|
|
91
150
|
readonly openFile: ((address: string) => void) | undefined;
|
|
151
|
+
readonly useTreeSelection: FilesTreeSurface['useTreeSelection'] | undefined;
|
|
152
|
+
readonly selectRoot: FilesTreeSurface['selectRoot'] | undefined;
|
|
153
|
+
readonly takeover: FilesTreeSurface['takeover'] | undefined;
|
|
92
154
|
}, t: FilesTreeProps['t']): FilesTreeSurface | undefined;
|
|
93
155
|
export { filesTreeSurfaceSupported, NS as FILES_TREE_NS };
|
|
94
156
|
export { FilesTree } from './FilesTree.tsx';
|
package/lib/types/registry.d.ts
CHANGED
|
@@ -70,6 +70,28 @@ export declare const REGISTRY_CONTENDED_MESSAGE = "the root registry is owned by
|
|
|
70
70
|
export declare const DEFAULT_MAX_ADDITIONAL_ROOTS = 16;
|
|
71
71
|
/** Hard ceiling for `maxAdditionalRoots`: values above it are config-invalid. */
|
|
72
72
|
export declare const MAX_ADDITIONAL_ROOTS_CEILING = 64;
|
|
73
|
+
/**
|
|
74
|
+
* The recovery class of a `storage-failed` authority: `retryable` failures
|
|
75
|
+
* (open/listen/acquire) may be re-attempted by `ensureAuthority`, while
|
|
76
|
+
* `config` (a row the operator must fix) and `quarantine` (an unknown durable
|
|
77
|
+
* outcome, isolated until restart) make the state terminal.
|
|
78
|
+
*/
|
|
79
|
+
export type StorageFailureRecovery = 'retryable' | 'config' | 'quarantine';
|
|
80
|
+
/** The recovery class a degraded authority belongs to (see {@link StorageFailureRecovery}). */
|
|
81
|
+
export type RecoveryClass = 'retryable' | 'config' | 'quarantine';
|
|
82
|
+
/** Operator-facing read-only picture of the registry authority and its recovery. */
|
|
83
|
+
export interface RegistryDiagnostics {
|
|
84
|
+
readonly authorityState: RegistryAuthorityState;
|
|
85
|
+
readonly epoch: string | undefined;
|
|
86
|
+
readonly connectionFailure: string | undefined;
|
|
87
|
+
readonly recoveryClass: RecoveryClass | undefined;
|
|
88
|
+
readonly recovery: {
|
|
89
|
+
readonly phase: 'idle' | 'waiting' | 'attempting' | 'disabled' | 'requires-restart';
|
|
90
|
+
readonly attempts: number;
|
|
91
|
+
readonly nextRetryAt: number | undefined;
|
|
92
|
+
readonly pendingSync: number;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
73
95
|
/**
|
|
74
96
|
* Whether this process is the Registry Authority for the store-wide domain
|
|
75
97
|
* medium. Contended and storage-failed are both fail-closed: the scope stays
|
|
@@ -85,6 +107,7 @@ export type RegistryAuthorityState = {
|
|
|
85
107
|
} | {
|
|
86
108
|
readonly kind: 'storage-failed';
|
|
87
109
|
readonly reason: string;
|
|
110
|
+
readonly recovery: StorageFailureRecovery;
|
|
88
111
|
} | {
|
|
89
112
|
readonly kind: 'config-invalid';
|
|
90
113
|
readonly reason: string;
|
|
@@ -110,6 +133,12 @@ export interface Config {
|
|
|
110
133
|
* exactly what classification grants — only new adds are refused.
|
|
111
134
|
*/
|
|
112
135
|
readonly maxAdditionalRoots?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Whether the registry retries a degraded authority in the background
|
|
138
|
+
* (bounded backoff, `unref`ed). Defaults to `true`; `false` leaves
|
|
139
|
+
* recovery to on-demand calls (refresh, the panel Retry button).
|
|
140
|
+
*/
|
|
141
|
+
readonly backgroundRecovery?: boolean;
|
|
113
142
|
}
|
|
114
143
|
declare module '@deepseek-ai/cordis' {
|
|
115
144
|
interface Context {
|
|
@@ -206,8 +235,18 @@ export declare class MultiRootRegistry extends Service {
|
|
|
206
235
|
private client;
|
|
207
236
|
private connectionFailure;
|
|
208
237
|
private connectionGeneration;
|
|
238
|
+
/** Local cancel of the in-flight handshake; teardown must not wait out hello. */
|
|
239
|
+
private connectionAbort;
|
|
209
240
|
private remoteProjectionReady;
|
|
210
241
|
private readonly remoteRevisions;
|
|
242
|
+
/**
|
|
243
|
+
* Primary roots whose projection must still be synced after a follower
|
|
244
|
+
* reconnect — populated from the tracked cache keys at adoption, removed as
|
|
245
|
+
* each `applySnapshot` lands. Recovery completes only when it empties.
|
|
246
|
+
*/
|
|
247
|
+
private pendingSync;
|
|
248
|
+
/** Bounded background retry for degraded authority states (ADR-0026 R1). */
|
|
249
|
+
private readonly recovery;
|
|
211
250
|
private readonly operationProof;
|
|
212
251
|
/** Whether this process currently owns the store. */
|
|
213
252
|
private authorityState;
|
|
@@ -215,6 +254,8 @@ export declare class MultiRootRegistry extends Service {
|
|
|
215
254
|
private lease;
|
|
216
255
|
/** Open domain handle; present only while {@link authority} is `active`. */
|
|
217
256
|
private domain;
|
|
257
|
+
/** The last failed `openDomain` reason; a repeat failure is not re-logged. */
|
|
258
|
+
private lastOpenFailure;
|
|
218
259
|
/** The open table; present once this process is the authority. */
|
|
219
260
|
private table;
|
|
220
261
|
/** Last classified status list per canonical primary root. */
|
|
@@ -271,6 +312,23 @@ export declare class MultiRootRegistry extends Service {
|
|
|
271
312
|
get shared(): boolean;
|
|
272
313
|
get available(): boolean;
|
|
273
314
|
get authority(): RegistryAuthorityState;
|
|
315
|
+
/** The class a degraded authority belongs to; `undefined` when healthy. */
|
|
316
|
+
private get recoveryClass();
|
|
317
|
+
/**
|
|
318
|
+
* Read-only diagnostics for operators: the authority state, the epoch, the
|
|
319
|
+
* last connection failure, and what the recovery scheduler is doing.
|
|
320
|
+
* `requires-restart` marks classes no timer will ever fix — a `config`
|
|
321
|
+
* row or a `quarantine`d store must NOT read as "retrying". Purely a read;
|
|
322
|
+
* it never arms or runs anything.
|
|
323
|
+
*/
|
|
324
|
+
get diagnostics(): RegistryDiagnostics;
|
|
325
|
+
/** The last logged `(kind, recoveryClass)` pair; dedupes transition logs. */
|
|
326
|
+
private loggedRecovery;
|
|
327
|
+
/**
|
|
328
|
+
* Log one line per authority/recovery-class TRANSITION — never per tick:
|
|
329
|
+
* waiting↔attempting moves say nothing an operator did not already know.
|
|
330
|
+
*/
|
|
331
|
+
private noteRecoveryTransition;
|
|
274
332
|
/**
|
|
275
333
|
* The configured cap on stored additional-root records per primary root.
|
|
276
334
|
* Surfaces show it next to the count so "N of LIMIT" is always explainable.
|
|
@@ -423,6 +481,12 @@ export declare class MultiRootRegistry extends Service {
|
|
|
423
481
|
grantedFor(policy: SandboxExecutionPolicy): readonly string[];
|
|
424
482
|
/** Classify one stored record set and cache it. */
|
|
425
483
|
private accept;
|
|
484
|
+
/**
|
|
485
|
+
* The pure half of {@link accept}: classification plus the over-limit
|
|
486
|
+
* warning, without writing the cache or the scope. {@link openDomain}
|
|
487
|
+
* staging uses it so a half-opened domain cannot materialize rows.
|
|
488
|
+
*/
|
|
489
|
+
private classify;
|
|
426
490
|
/**
|
|
427
491
|
* The statuses of one key, from memory where possible: the cache, else what
|
|
428
492
|
* storage holds, else empty. A cached answer is never re-checked here — that
|
|
@@ -482,8 +546,42 @@ export declare class MultiRootRegistry extends Service {
|
|
|
482
546
|
private snapshotOf;
|
|
483
547
|
private captureSnapshot;
|
|
484
548
|
private notifyShared;
|
|
549
|
+
/** Depth of {@link commit}; nested calls share the outer round's buffers. */
|
|
550
|
+
private commitDepth;
|
|
551
|
+
/** Keys whose publish inside a commit produced deferred notifications, first-seen order. */
|
|
552
|
+
private readonly commitNotifications;
|
|
553
|
+
/**
|
|
554
|
+
* Run `body` as one Registry commit: scope writes inside it go through
|
|
555
|
+
* `multiRootScope.batch`, and the `notifyShared` broadcasts plus Registry
|
|
556
|
+
* listener calls `publish()` produces are collected per key instead of
|
|
557
|
+
* delivered. When the OUTERMOST commit ends — also when `body` throws — the
|
|
558
|
+
* scope batch flushes first, then the broadcasts go out in first-seen key
|
|
559
|
+
* order, then the listeners: no synchronous callback can observe a
|
|
560
|
+
* half-committed state.
|
|
561
|
+
*/
|
|
562
|
+
private commit;
|
|
563
|
+
/** Deliver one key's notifications now, or collect them inside a commit. */
|
|
564
|
+
private deliverNotifications;
|
|
485
565
|
private applySnapshot;
|
|
486
566
|
private remoteUnavailable;
|
|
567
|
+
/**
|
|
568
|
+
* Resynchronize each still-pending tracked primary outside the authority
|
|
569
|
+
* queue: per-key snapshots through the live client. A failed key stays in
|
|
570
|
+
* `pendingSync` for the next tick; a lost connection abandons the round —
|
|
571
|
+
* `remoteUnavailable` already emptied the set.
|
|
572
|
+
*/
|
|
573
|
+
private syncProjection;
|
|
574
|
+
/**
|
|
575
|
+
* Write one key's withheld-only projection into the scope, keeping
|
|
576
|
+
* `published` truthful: the signature records what the scope HOLDS, never
|
|
577
|
+
* what `publish()` last computed — a withheld signature can never equal a
|
|
578
|
+
* publish signature, so an empty snapshot afterwards really writes the
|
|
579
|
+
* retraction. A repeated write of the same withheld projection is a no-op
|
|
580
|
+
* (the same-signature skip is also the republish gate of a repeated
|
|
581
|
+
* downgrade). `metadataPublished` is untouched: a withheld write announces
|
|
582
|
+
* nothing to the Registry view.
|
|
583
|
+
*/
|
|
584
|
+
private withholdScope;
|
|
487
585
|
private remoteSnapshot;
|
|
488
586
|
private remote;
|
|
489
587
|
private dispatchShared;
|
package/lib/types/scope.d.ts
CHANGED
|
@@ -208,6 +208,10 @@ export declare class MultiRootScopeService extends Service {
|
|
|
208
208
|
/** Additional roots keyed by canonical primary root; empty until roots are registered. */
|
|
209
209
|
private readonly rootsByPrimary;
|
|
210
210
|
private readonly listeners;
|
|
211
|
+
/** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
|
|
212
|
+
private batchDepth;
|
|
213
|
+
/** True when a batched write coalesced its notification into the flush. */
|
|
214
|
+
private batchDirty;
|
|
211
215
|
private admissionProof;
|
|
212
216
|
/**
|
|
213
217
|
* The registry-authority fact the registry row publishes. Defaults to
|
|
@@ -231,6 +235,20 @@ export declare class MultiRootScopeService extends Service {
|
|
|
231
235
|
private readonly dshHome;
|
|
232
236
|
/** Observe registry publications; consumers still resolve authority from scopeOf(). */
|
|
233
237
|
subscribe(listener: () => void): () => void;
|
|
238
|
+
/**
|
|
239
|
+
* Run `body` as one notification round: `setAdditionalRoots()` and
|
|
240
|
+
* `setRegistryAuthority()` apply their state immediately (a same-value
|
|
241
|
+
* authority write still stays silent), but subscriber invocations coalesce
|
|
242
|
+
* into a single flush when the OUTERMOST batch ends — also when `body`
|
|
243
|
+
* throws, because the state already landed. A write a subscriber makes
|
|
244
|
+
* during the flush is not part of the batch and notifies synchronously
|
|
245
|
+
* under the normal rules.
|
|
246
|
+
* @param body - the writes to run as one round.
|
|
247
|
+
* @returns the body's return value.
|
|
248
|
+
*/
|
|
249
|
+
batch<T>(body: () => T): T;
|
|
250
|
+
/** Notify subscribers now, or coalesce into the enclosing batch's flush. */
|
|
251
|
+
private notifySubscribers;
|
|
234
252
|
/**
|
|
235
253
|
* The sorted names of the tools `toolAccess` caps — read-only for the
|
|
236
254
|
* prompt contribution. An invalid config is not enumerated (it caps every
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-electron/dsh-plugin-multi-root-workspace",
|
|
3
3
|
"description": "Multi-root Workspace for DeepSeek Harness: adds N additional roots to the workspace sandbox scope as an out-of-tree bundle",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|