@deepseek-ai/dsh-api-workspace-files 0.1.6-alpha.2 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,23 @@
1
1
  /**
2
- * Producer of the `changes` stream: every `fs/observed` emission whose target
3
- * lies inside a generation's workspace root becomes one frame of that
4
- * generation. Instrumented filesystem operations emit these observations; the
5
- * operating system is not watched.
6
- * Each generation acknowledges its observation queue and resolved workspace
7
- * root with `ready` before emitting any queued or live changes.
2
+ * Target-scoped filesystem watches and `fs/observed` invalidations for `changes`.
3
+ * Each generation sends `ready` after watcher initialization, then reads current
4
+ * target metadata for matching queued and live invalidations.
8
5
  */
9
6
  import type { Context } from '@deepseek-ai/cordis';
10
7
  import type { WorkspaceFileWatchFrame } from './types.ts';
11
- /** Owns `fs/observed` observation and every open `changes` generation. */
8
+ /** Owns target watches, instrumented observations, and every open `changes` generation. */
12
9
  export declare class WorkspaceChangeFeed {
13
10
  private readonly ctx;
14
11
  private readonly followers;
15
12
  /** @param ctx - Host context carrying the filesystem the observations come from. */
16
13
  constructor(ctx: Context);
17
14
  /**
18
- * Open one generation reporting observations inside `workspaceRoot`.
15
+ * Open one target watch; directory targets remain inside `workspaceRoot`.
19
16
  * @param workspaceRoot - the session's workspace root path.
17
+ * @param path - target path, resolved relative to the workspace root; Host metadata determines its type.
20
18
  * @param signal - generation cancellation.
21
- * @returns `ready` after observation is active and the root resolves, then
22
- * observations made after the generation was first pulled, in emission order.
19
+ * @returns `ready` after watching starts, then current metadata for target invalidations.
23
20
  */
24
- follow(workspaceRoot: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
21
+ follow(workspaceRoot: string, path: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
25
22
  }
26
23
  //# sourceMappingURL=changes.d.ts.map
@@ -1,13 +1,11 @@
1
1
  /**
2
- * Producer of the `changes` stream: every `fs/observed` emission whose target
3
- * lies inside a generation's workspace root becomes one frame of that
4
- * generation. Instrumented filesystem operations emit these observations; the
5
- * operating system is not watched.
6
- * Each generation acknowledges its observation queue and resolved workspace
7
- * root with `ready` before emitting any queued or live changes.
2
+ * Target-scoped filesystem watches and `fs/observed` invalidations for `changes`.
3
+ * Each generation sends `ready` after watcher initialization, then reads current
4
+ * target metadata for matching queued and live invalidations.
8
5
  */
9
6
  import { Deque } from '@deepseek-ai/dsh-deque';
10
- /** Owns `fs/observed` observation and every open `changes` generation. */
7
+ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
8
+ /** Owns target watches, instrumented observations, and every open `changes` generation. */
11
9
  export class WorkspaceChangeFeed {
12
10
  ctx;
13
11
  followers = new Set();
@@ -18,92 +16,154 @@ export class WorkspaceChangeFeed {
18
16
  for (const follower of this.followers)
19
17
  follower.push([target, observation]);
20
18
  });
21
- ctx.effect(() => () => {
22
- for (const follower of this.followers)
23
- follower.close();
19
+ ctx.effect(() => async () => {
20
+ const followers = [...this.followers];
21
+ await Promise.all(followers.map(follower => follower.close()));
24
22
  this.followers.clear();
25
23
  }, 'workspace-files.changes');
26
24
  }
27
25
  /**
28
- * Open one generation reporting observations inside `workspaceRoot`.
26
+ * Open one target watch; directory targets remain inside `workspaceRoot`.
29
27
  * @param workspaceRoot - the session's workspace root path.
28
+ * @param path - target path, resolved relative to the workspace root; Host metadata determines its type.
30
29
  * @param signal - generation cancellation.
31
- * @returns `ready` after observation is active and the root resolves, then
32
- * observations made after the generation was first pulled, in emission order.
30
+ * @returns `ready` after watching starts, then current metadata for target invalidations.
33
31
  */
34
- async *follow(workspaceRoot, signal) {
32
+ async *follow(workspaceRoot, path, signal) {
35
33
  signal.throwIfAborted();
36
- // Registered before the root resolves, so nothing observed while it does is
37
- // missed; the root only filters at drain time.
38
- const follower = new ChangeFollower();
34
+ // Instrumented observations remain queued while the target resolves.
35
+ const follower = new ChangeFollower(() => {
36
+ signal.removeEventListener('abort', cancel);
37
+ this.followers.delete(follower);
38
+ });
39
+ signal = AbortSignal.any([signal, follower.controller.signal]);
40
+ const aborted = () => signal.aborted;
41
+ const cancel = () => {
42
+ void follower.close().catch((error) => { this.ctx.logger.error(error); });
43
+ };
44
+ signal.addEventListener('abort', cancel, { once: true });
39
45
  this.followers.add(follower);
46
+ let unwatch;
40
47
  try {
41
- // Under the generation's signal, so a consumer leaving mid-resolve on a slow
42
- // backend releases the follower now rather than when the resolve settles;
43
- // a rejection the abort caused is the quiet end every other abort takes here.
48
+ // Setup shares cancellation, so a late resolution cannot acquire a watcher.
44
49
  const root = await this.ctx.fs.resolve(workspaceRoot, { signal }).catch((error) => {
45
- if (signal.aborted)
50
+ if (aborted())
51
+ return undefined;
52
+ throw error;
53
+ });
54
+ if (root === undefined || aborted() || follower.isClosed)
55
+ return;
56
+ const target = await this.ctx.fs.resolve(path, { cwd: workspaceRoot, signal }).catch((error) => {
57
+ if (aborted())
46
58
  return undefined;
47
59
  throw error;
48
60
  });
49
- if (root === undefined || signal.aborted || follower.isClosed)
61
+ if (target === undefined || aborted())
62
+ return;
63
+ const stat = async () => {
64
+ const info = await this.ctx.fs.stat(target, signal).catch((error) => {
65
+ if (aborted())
66
+ return undefined;
67
+ throw error;
68
+ });
69
+ if (info?.type === 'directory' && !this.ctx.fs.contains(root, target)) {
70
+ throw new RemoteError('workspace-file/outside-workspace', 'Directory is outside the workspace', { path });
71
+ }
72
+ return info;
73
+ };
74
+ await stat();
75
+ if (aborted())
76
+ return;
77
+ try {
78
+ unwatch = await this.ctx.fs.watch(target, (error) => {
79
+ if (error !== undefined)
80
+ follower.fail(error);
81
+ else if (!follower.isClosed)
82
+ follower.push([target]);
83
+ }, signal);
84
+ if (follower.error !== undefined)
85
+ throw follower.error;
86
+ }
87
+ catch (error) {
88
+ if (aborted() && follower.error === undefined)
89
+ return;
90
+ const failure = follower.error ?? error;
91
+ throw new RemoteError('workspace-file/watch-unsupported', failure instanceof Error ? failure.message : String(failure), { path });
92
+ }
93
+ follower.initialized.resolve(unwatch);
94
+ if (aborted())
50
95
  return;
51
96
  yield { kind: 'ready' };
52
- for await (const [target, observation] of follower.read(signal)) {
53
- if (!this.ctx.fs.contains(root, target))
97
+ for await (const [observed] of follower.read()) {
98
+ if (observed.targetKey !== target.targetKey)
54
99
  continue;
100
+ const info = await stat();
101
+ if (aborted())
102
+ return;
55
103
  const absolutePath = this.ctx.fs.processPath(target);
56
104
  yield {
57
105
  kind: 'change',
58
- change: observation.kind === 'present'
59
- ? { absolutePath, version: observation.version }
106
+ change: info !== undefined
107
+ ? { absolutePath, version: info.version }
60
108
  : { absolutePath, absent: true },
61
109
  };
62
110
  }
63
111
  }
64
112
  finally {
65
- this.followers.delete(follower);
66
- follower.close();
113
+ follower.initialized.resolve(unwatch);
114
+ await follower.close();
67
115
  }
68
116
  }
69
117
  }
70
118
  /** One generation's queue: observations wait here until its consumer pulls them. */
71
119
  class ChangeFollower {
120
+ release;
121
+ controller = new AbortController();
122
+ initialized = Promise.withResolvers();
123
+ error;
72
124
  queue = new Deque();
73
125
  wake;
74
126
  closed = false;
75
- /** Whether the generation was closed while its workspace root resolved. */
127
+ closing;
128
+ constructor(release) {
129
+ this.release = release;
130
+ }
131
+ /** Whether this generation has stopped accepting invalidations. */
76
132
  get isClosed() {
77
133
  return this.closed;
78
134
  }
79
135
  push(observed) {
136
+ if (this.closed)
137
+ return;
80
138
  this.queue.pushBack(observed);
81
139
  this.wake?.();
82
140
  }
83
141
  close() {
142
+ if (this.closing !== undefined)
143
+ return this.closing;
84
144
  this.closed = true;
145
+ this.closing = this.initialized.promise.then(unwatch => unwatch?.()).finally(this.release);
146
+ this.controller.abort();
85
147
  this.wake?.();
148
+ return this.closing;
149
+ }
150
+ fail(error) {
151
+ this.error = error;
152
+ this.controller.abort();
86
153
  }
87
154
  /** Drain until closed or aborted; anything still queued then is dropped with the generation. */
88
- async *read(signal) {
89
- const abort = () => { this.close(); };
90
- signal.addEventListener('abort', abort, { once: true });
91
- if (signal.aborted)
92
- abort();
93
- try {
94
- while (!this.closed) {
95
- const observed = this.queue.popFront();
96
- if (observed !== undefined) {
97
- yield observed;
98
- continue;
99
- }
100
- await new Promise((resolve) => { this.wake = resolve; });
101
- this.wake = undefined;
155
+ async *read() {
156
+ while (!this.closed) {
157
+ const observed = this.queue.popFront();
158
+ if (observed !== undefined) {
159
+ yield observed;
160
+ continue;
102
161
  }
162
+ await new Promise((resolve) => { this.wake = resolve; });
163
+ this.wake = undefined;
103
164
  }
104
- finally {
105
- signal.removeEventListener('abort', abort);
106
- }
165
+ if (this.error !== undefined)
166
+ throw this.error;
107
167
  }
108
168
  }
109
169
  //# sourceMappingURL=changes.js.map
@@ -1,11 +1,11 @@
1
1
  /**
2
- * One Host `changes` subscription per session, fanned out to the open files of
3
- * that session.
2
+ * One Host `changes` subscription per Session and requested path, shared by
3
+ * that target's open resources.
4
4
  *
5
- * The Host reports every agent write in a session on one stream; each open file
6
- * wants only its own. The feed opens the session stream when the first follower
5
+ * The Host reports invalidations from filesystem watches and instrumented writes.
6
+ * The feed opens the target stream when the first follower
7
7
  * arrives, hands each frame to the followers of its path, and disposes the
8
- * stream when the last follower leaves. A follower buffers session changes
8
+ * stream when the last follower leaves. A follower buffers target changes
9
9
  * until `stat` supplies its Host absolute path, then filters queued and live
10
10
  * frames by that path, with `\\` normalized to `/`.
11
11
  */
@@ -50,7 +50,7 @@ declare class Follower implements AsyncIterable<WorkspaceFileNotice> {
50
50
  [Symbol.asyncIterator](): AsyncIterator<WorkspaceFileNotice>;
51
51
  }
52
52
  /**
53
- * Per-session fan-out of the Host's workspace file change stream.
53
+ * Per-target sharing of the Host's workspace file change streams.
54
54
  *
55
55
  * Owned by the provider; one instance serves every session of the Client.
56
56
  */
@@ -58,7 +58,7 @@ export declare class ChangeFeed {
58
58
  private readonly remote;
59
59
  /** Live feeds only: a feed removes itself when its stream closes. */
60
60
  private readonly sessions;
61
- /** Streams still closing, by session: the session's next feed opens after its predecessor has settled. */
61
+ /** Streams still closing, by Session and path; a successor waits for its predecessor. */
62
62
  private readonly closing;
63
63
  /**
64
64
  * @param remote - the Remote face carrying `$stream` and `workspaceFiles.changes`.
@@ -69,18 +69,19 @@ export declare class ChangeFeed {
69
69
  *
70
70
  * The follower is registered on call, not on first pull. Changes delivered
71
71
  * to this Client are queued while stat is pending. The first follower starts
72
- * the session's local `changes` call. The iterable ends
73
- * when `signal` aborts or when the session stream is gone; ending it early
72
+ * target's local `changes` call. The iterable ends
73
+ * when `signal` aborts or when the target stream is gone; ending it early
74
74
  * (`break`, `return`) unregisters the follower as well, and the last follower
75
- * of a session disposes its stream. Await a true `ready` result before stat
75
+ * of a target disposes its stream. Await a true `ready` result before stat
76
76
  * so the Host subscription is active, then bind each stat's absolute path. Until binding,
77
- * any session write can trigger a retry; after binding, only matching queued
77
+ * any target invalidation can trigger a retry; after binding, only matching queued
78
78
  * and live changes pass.
79
- * @param sessionId - the session whose workspace holds the file.
79
+ * @param sessionId - the Session providing the file's read authority.
80
+ * @param path - requested file path; followers share a stream only for the same Session and path.
80
81
  * @param signal - ends the follow.
81
82
  * @returns a single-consumer subscription with Host-path binding and explicit disposal.
82
83
  */
83
- follow(sessionId: SessionId, signal: AbortSignal): Follower;
84
+ follow(sessionId: SessionId, path: string, signal: AbortSignal): Follower;
84
85
  /**
85
86
  * Wait for every stream that is still closing, so an owner tearing down
86
87
  * leaves no Host stream behind.
@@ -61,7 +61,7 @@ class Follower {
61
61
  while (true) {
62
62
  const next = this.pending.shift();
63
63
  if (next !== undefined) {
64
- if (this.hostKey === undefined || next.key === this.hostKey)
64
+ if (next.notice.kind === 'refresh' || this.hostKey === undefined || next.key === this.hostKey)
65
65
  yield next.notice;
66
66
  continue;
67
67
  }
@@ -76,31 +76,32 @@ class Follower {
76
76
  }
77
77
  }
78
78
  }
79
- /** The stream and followers of one session. */
79
+ /** The stream and followers of one Session and requested path. */
80
80
  class SessionFeed {
81
81
  onClose;
82
82
  followers = new Set();
83
83
  stream;
84
84
  closed = false;
85
85
  started = false;
86
+ acknowledged = false;
86
87
  /**
87
88
  * @param remote - the Remote face carrying `workspaceFiles.changes`.
88
- * @param sessionId - the session whose writes this feed follows.
89
- * @param after - the previous feed of this session still closing, if any; the stream opens once it has settled.
89
+ * @param sessionId - the Session whose filesystem this feed observes.
90
+ * @param path - target path submitted to the Host.
91
+ * @param after - the previous feed of this target still closing, if any; the stream opens once it has settled.
90
92
  * @param onClose - called once when the stream is gone, whatever the cause, with the dispose that is closing it.
91
93
  */
92
- constructor(remote, sessionId, after, onClose) {
94
+ constructor(remote, sessionId, path, after, onClose) {
93
95
  this.onClose = onClose;
94
96
  this.stream = remote.$stream({
95
97
  name: `workspace file changes of ${sessionId}`,
96
- // A predecessor still closing finishes first, so one session never has
98
+ // A predecessor still closing finishes first, so one target never has
97
99
  // two Host streams open at once.
98
100
  open: (signal) => {
99
101
  this.started = false;
100
- return openAfter(after, () => remote.workspaceFiles.changes(sessionId, signal));
102
+ return openAfter(after, () => remote.workspaceFiles.changes(sessionId, path, signal));
101
103
  },
102
- // A normal end means the Host closed the session's feed: the session is
103
- // gone or the Host is shutting down, so there is nothing to reopen.
104
+ // A normal end means the Host closed the target's feed.
104
105
  ended: () => new Error(`workspace file changes of ${sessionId} ended`),
105
106
  });
106
107
  void this.pump();
@@ -131,8 +132,12 @@ class SessionFeed {
131
132
  case 'ready':
132
133
  item.accept();
133
134
  this.started = true;
134
- for (const follower of this.followers)
135
+ for (const follower of this.followers) {
135
136
  follower.start();
137
+ if (this.acknowledged)
138
+ follower.push({ kind: 'refresh' }, '');
139
+ }
140
+ this.acknowledged = true;
136
141
  break;
137
142
  case 'change': {
138
143
  const key = keyOf(frame.change.absolutePath);
@@ -187,7 +192,7 @@ function assertNever(frame) {
187
192
  throw new Error(`Unexpected workspace file watch frame: ${JSON.stringify(frame)}`);
188
193
  }
189
194
  /**
190
- * Per-session fan-out of the Host's workspace file change stream.
195
+ * Per-target sharing of the Host's workspace file change streams.
191
196
  *
192
197
  * Owned by the provider; one instance serves every session of the Client.
193
198
  */
@@ -195,7 +200,7 @@ export class ChangeFeed {
195
200
  remote;
196
201
  /** Live feeds only: a feed removes itself when its stream closes. */
197
202
  sessions = new Map();
198
- /** Streams still closing, by session: the session's next feed opens after its predecessor has settled. */
203
+ /** Streams still closing, by Session and path; a successor waits for its predecessor. */
199
204
  closing = new Map();
200
205
  /**
201
206
  * @param remote - the Remote face carrying `$stream` and `workspaceFiles.changes`.
@@ -208,19 +213,20 @@ export class ChangeFeed {
208
213
  *
209
214
  * The follower is registered on call, not on first pull. Changes delivered
210
215
  * to this Client are queued while stat is pending. The first follower starts
211
- * the session's local `changes` call. The iterable ends
212
- * when `signal` aborts or when the session stream is gone; ending it early
216
+ * target's local `changes` call. The iterable ends
217
+ * when `signal` aborts or when the target stream is gone; ending it early
213
218
  * (`break`, `return`) unregisters the follower as well, and the last follower
214
- * of a session disposes its stream. Await a true `ready` result before stat
219
+ * of a target disposes its stream. Await a true `ready` result before stat
215
220
  * so the Host subscription is active, then bind each stat's absolute path. Until binding,
216
- * any session write can trigger a retry; after binding, only matching queued
221
+ * any target invalidation can trigger a retry; after binding, only matching queued
217
222
  * and live changes pass.
218
- * @param sessionId - the session whose workspace holds the file.
223
+ * @param sessionId - the Session providing the file's read authority.
224
+ * @param path - requested file path; followers share a stream only for the same Session and path.
219
225
  * @param signal - ends the follow.
220
226
  * @returns a single-consumer subscription with Host-path binding and explicit disposal.
221
227
  */
222
- follow(sessionId, signal) {
223
- const feed = signal.aborted ? undefined : this.feedOf(sessionId);
228
+ follow(sessionId, path, signal) {
229
+ const feed = signal.aborted ? undefined : this.feedOf(sessionId, path);
224
230
  const leave = () => {
225
231
  signal.removeEventListener('abort', leave);
226
232
  follower.end();
@@ -244,20 +250,21 @@ export class ChangeFeed {
244
250
  async settle() {
245
251
  await Promise.all(this.closing.values());
246
252
  }
247
- feedOf(sessionId) {
248
- const existing = this.sessions.get(sessionId);
253
+ feedOf(sessionId, path) {
254
+ const key = JSON.stringify([sessionId, path]);
255
+ const existing = this.sessions.get(key);
249
256
  if (existing !== undefined)
250
257
  return existing;
251
- const feed = new SessionFeed(this.remote, sessionId, this.closing.get(sessionId), (closed) => {
252
- this.sessions.delete(sessionId);
258
+ const feed = new SessionFeed(this.remote, sessionId, path, this.closing.get(key), (closed) => {
259
+ this.sessions.delete(key);
253
260
  // A dispose that rejects is still a settled close: nothing remains to wait for.
254
261
  const tracked = closed.then(() => undefined, () => undefined).then(() => {
255
- if (this.closing.get(sessionId) === tracked)
256
- this.closing.delete(sessionId);
262
+ if (this.closing.get(key) === tracked)
263
+ this.closing.delete(key);
257
264
  });
258
- this.closing.set(sessionId, tracked);
265
+ this.closing.set(key, tracked);
259
266
  });
260
- this.sessions.set(sessionId, feed);
267
+ this.sessions.set(key, feed);
261
268
  return feed;
262
269
  }
263
270
  }
@@ -13,9 +13,9 @@
13
13
  * a string outside the grammar, `workspace-file/unknown-workspace` when the
14
14
  * address carries no Session — and ends.
15
15
  *
16
- * The first frame is the file's `stat`; every Host-reported write yields the
17
- * metadata with its reported version; a reported disappearance, or a write while the
18
- * last stat had failed, runs `stat` again. Failures travel as `ok: false` frames, never as thrown errors: the
16
+ * The first frame is the file's `stat`; subsequent invalidations run `stat`
17
+ * again unless their version or absence is already known. Reconnection also
18
+ * restats the file. Failures travel as `ok: false` frames, never as thrown errors: the
19
19
  * Remote face does not reject, and anything thrown inside the stream is a
20
20
  * programming error the resource model lets surface. A failed stat does not end
21
21
  * the stream: the next write stats again. One {@link ChangeFeed}
@@ -27,7 +27,7 @@ import type { WorkspaceFilesRemote } from './remote.ts';
27
27
  /**
28
28
  * Build the `file` provider over one Remote face and one change feed.
29
29
  * @param remote - the Remote face carrying `workspaceFiles.stat`.
30
- * @param changes - the per-session change fan-out.
30
+ * @param changes - target-scoped change streams shared by file resources.
31
31
  * @returns the provider to register into `ctx.resources`.
32
32
  */
33
33
  export declare function createFileResourceProvider(remote: WorkspaceFilesRemote, changes: ChangeFeed): ResourceProvider<'file'>;
@@ -3,7 +3,7 @@ import { parseFileAddress } from '@deepseek-ai/dsh-util-workspace-path';
3
3
  /**
4
4
  * Build the `file` provider over one Remote face and one change feed.
5
5
  * @param remote - the Remote face carrying `workspaceFiles.stat`.
6
- * @param changes - the per-session change fan-out.
6
+ * @param changes - target-scoped change streams shared by file resources.
7
7
  * @returns the provider to register into `ctx.resources`.
8
8
  */
9
9
  export function createFileResourceProvider(remote, changes) {
@@ -17,7 +17,7 @@ export function createFileResourceProvider(remote, changes) {
17
17
  }
18
18
  const { sessionId, path } = resolved.value;
19
19
  // Queue changes delivered to this Client while stat is pending.
20
- const notices = changes.follow(sessionId, signal);
20
+ const notices = changes.follow(sessionId, path, signal);
21
21
  const stat = () => remote.workspaceFiles.stat(sessionId, path, signal);
22
22
  // Read through a call: a plain `signal.aborted` is narrowed to `false` by
23
23
  // the first check and would read as always-false after the later awaits.
@@ -26,7 +26,15 @@ export function createFileResourceProvider(remote, changes) {
26
26
  // on the file, so a write can still bring the file live.
27
27
  let current;
28
28
  try {
29
- if (!await notices.ready || aborted())
29
+ if (!await notices.ready) {
30
+ if (aborted())
31
+ return;
32
+ const result = await stat();
33
+ if (!aborted())
34
+ yield result;
35
+ return;
36
+ }
37
+ if (aborted())
30
38
  return;
31
39
  const first = await stat();
32
40
  if (aborted())
@@ -40,6 +48,8 @@ export function createFileResourceProvider(remote, changes) {
40
48
  yield first;
41
49
  }
42
50
  for await (const notice of notices) {
51
+ if (aborted())
52
+ return;
43
53
  if (current === undefined) {
44
54
  // Still gone: nothing new to report.
45
55
  if (notice.kind === 'absent')
@@ -50,9 +60,6 @@ export function createFileResourceProvider(remote, changes) {
50
60
  // consumer learns nothing new.
51
61
  if (notice.version === current.version)
52
62
  continue;
53
- current = { ...current, version: notice.version };
54
- yield { ok: true, value: current };
55
- continue;
56
63
  }
57
64
  // A Host notice may mean stale content.
58
65
  const again = await stat();
@@ -34,13 +34,15 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
34
34
  };
35
35
  }
36
36
  }
37
- /** One Host-reported write inside the session's workspace. */
37
+ /** One Host-reported target change in the Session's filesystem. */
38
38
  export type WorkspaceFileEdit = {
39
39
  readonly kind: 'changed';
40
40
  readonly version: string;
41
41
  } | {
42
42
  readonly kind: 'absent';
43
43
  };
44
- /** What one follower of a path receives: a Host write. */
45
- export type WorkspaceFileNotice = WorkspaceFileEdit;
44
+ /** A target change or a request to restat after reconnection. */
45
+ export type WorkspaceFileNotice = WorkspaceFileEdit | {
46
+ readonly kind: 'refresh';
47
+ };
46
48
  //# sourceMappingURL=types.d.ts.map
@@ -22,7 +22,7 @@ import type { Context } from '@deepseek-ai/cordis';
22
22
  import z from '@deepseek-ai/schemastery';
23
23
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
24
24
  import { TypertRemoteService, type TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
25
- import type { WorkspaceByteRange, WorkspaceDirectoryListing, WorkspaceFileBytes, WorkspaceFileRange, WorkspaceFileStat, WorkspaceFileText, WorkspaceFileWatchFrame } from './types.ts';
25
+ import type { WorkspaceByteReadOptions, WorkspaceDirectoryListing, WorkspaceFileBytes, WorkspaceFileRange, WorkspaceFileStat, WorkspaceFileText, WorkspaceFileWatchFrame } from './types.ts';
26
26
  export type * from './types.ts';
27
27
  declare module '@deepseek-ai/cordis' {
28
28
  interface Context {
@@ -81,32 +81,14 @@ export declare class WorkspaceFiles extends TypertRemoteService {
81
81
  */
82
82
  read(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>;
83
83
  /**
84
- * Read one byte window of a regular file readable by the filesystem backend: raw
85
- * bytes, no text decoding and no binary rejection.
84
+ * Read a complete regular file or one byte range without text decoding.
86
85
  * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
87
- * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
88
- * @param range - the byte window; omitted fields take the window defaults.
89
- * @param signal - caller cancellation.
90
- * @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
91
- */
92
- readBytes(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceByteRange, signal: AbortSignal): Promise<WorkspaceFileBytes>;
93
- /**
94
- * Read a complete regular file as bytes, subject to the configured full-file cap.
95
- * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
96
- * @param path - absolute or workspace-relative file path.
97
- * @param signal - caller cancellation.
98
- * @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
99
- */
100
- readAll(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>;
101
- /**
102
- * Read a complete file relative to another file's directory, including outside the workspace.
103
- * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
104
- * @param path - base file, absolute or workspace-relative.
105
- * @param relativePath - relative filesystem path, not a URL or absolute path.
86
+ * @param path - target path, absolute or workspace-relative; relative to the base file's directory when provided.
87
+ * @param options - optional base file and range; without a range the complete-file cap applies.
106
88
  * @param signal - caller cancellation.
107
- * @returns the complete related file using the ordinary file-size and access checks.
89
+ * @returns native bytes with the file's version and size at the preceding stat, byte offset, and EOF marker.
108
90
  */
109
- readRelated(workspaceFileScope: WorkspaceFileScope, path: string, relativePath: string, signal: AbortSignal): Promise<WorkspaceFileBytes>;
91
+ readBytes(workspaceFileScope: WorkspaceFileScope, path: string, options: WorkspaceByteReadOptions, signal: AbortSignal): Promise<WorkspaceFileBytes>;
110
92
  /**
111
93
  * Report one regular file's identity, version, and size without its content.
112
94
  * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
@@ -124,15 +106,16 @@ export declare class WorkspaceFiles extends TypertRemoteService {
124
106
  */
125
107
  list(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceDirectoryListing>;
126
108
  /**
127
- * Stream every `fs/observed` observation of a file inside the Session's
128
- * workspace. Only instrumented filesystem operations report here; the OS is
129
- * not watched.
109
+ * Watch one file or a directory's direct entries in the Session's filesystem.
110
+ * Files use the backend's read authority; directories remain workspace-scoped.
130
111
  * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
112
+ * @param path - target path; the Host determines its type and confines directories to the workspace.
131
113
  * @param signal - generation cancellation.
132
- * @returns `ready` once the Host observation queue is active and the workspace
133
- * root is resolved, then queued and live observations in emission order.
114
+ * @returns `ready` once the target watch is active, then current metadata for queued and live invalidations.
115
+ * @throws RemoteError when watching is unavailable or a directory is outside the workspace.
134
116
  */
135
- changes(workspaceFileScope: WorkspaceFileScope, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
117
+ changes(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
118
+ private relativePath;
136
119
  /** Apply the page defaults and caps here, so the request never carries them implicitly. */
137
120
  private resolvePage;
138
121
  /** Apply the byte-window defaults and cap; a window above the cap is refused, not shortened. */