@sparkelf/dsh-patch-workspace-storage-restore 0.2.0-rc.3 → 0.2.0-rc.30

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/package.json CHANGED
@@ -4,11 +4,11 @@
4
4
  "formatVersion": 1,
5
5
  "variants": [
6
6
  {
7
- "dsh": ">=0.1.6-alpha.1",
7
+ "dsh": ">=0.1.7-rc.2",
8
8
  "file": "./patches/workspace-storage-restore.patch",
9
9
  "id": "workspace-storage-restore",
10
10
  "target": {
11
- "baseRevision": "0a15e36e7f82b6ed45af6fa9759f29b40dcd965d",
11
+ "baseRevision": "477b4f420553e8a52c2fbccc464d7561b239c443",
12
12
  "kind": "dsh-source",
13
13
  "paths": [
14
14
  "packages/workspace/workspace/"
@@ -35,5 +35,5 @@
35
35
  "url": "git+https://github.com/SparkElf/deepseek-harness-plus.git"
36
36
  },
37
37
  "type": "module",
38
- "version": "0.2.0-rc.3"
38
+ "version": "0.2.0-rc.30"
39
39
  }
@@ -1,8 +1,8 @@
1
1
  diff --git a/packages/workspace/workspace/src/index.ts b/packages/workspace/workspace/src/index.ts
2
- index 037344fce4..7b6d915807 100644
2
+ index 00eaf0030c..b6e761e1cb 100644
3
3
  --- a/packages/workspace/workspace/src/index.ts
4
4
  +++ b/packages/workspace/workspace/src/index.ts
5
- @@ -10,7 +10,7 @@ import { stat } from 'node:fs/promises'
5
+ @@ -10,7 +10,7 @@ import { mkdir, stat } from 'node:fs/promises'
6
6
  import { Context, Service } from '@deepseek-ai/cordis'
7
7
  import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
8
8
  import type {} from '@deepseek-ai/dsh-session-persistence'
@@ -11,7 +11,7 @@ index 037344fce4..7b6d915807 100644
11
11
  import { WorkspaceEntity } from './entity.ts'
12
12
  import type { WorkspaceEntityHost } from './entity.ts'
13
13
 
14
- @@ -91,6 +91,7 @@ const compareHeaders = (left: SessionHeader, right: SessionHeader): number =>
14
+ @@ -170,6 +170,7 @@ const compareHeaders = (left: SessionHeader, right: SessionHeader): number =>
15
15
  export class WorkspaceRegistry extends Service {
16
16
  static inject = ['storageDomain', 'sessionPersistence']
17
17
 
@@ -19,7 +19,7 @@ index 037344fce4..7b6d915807 100644
19
19
  private table?: KvTable<WorkspaceId, WorkspaceRecord>
20
20
  private global?: DomainGlobal<WorkspaceDomainState>
21
21
  private state?: WorkspaceDomainState
22
- @@ -116,8 +117,51 @@ export class WorkspaceRegistry extends Service {
22
+ @@ -195,8 +196,53 @@ export class WorkspaceRegistry extends Service {
23
23
 
24
24
  /** Open the domain, finish bootstrap when required, and rebuild the ordered cache. */
25
25
  protected async [Service.init](): Promise<void> {
@@ -36,10 +36,12 @@ index 037344fce4..7b6d915807 100644
36
36
  + * @returns the operation result after the new durable baseline is available.
37
37
  + */
38
38
  + async withStorageRestore<T>(restore: () => Promise<T>): Promise<T> {
39
- + // restore替换durable files前必须先退出domain;finally复用startup路径重建全部index。
39
+ + // The restore replaces durable files, so the domain must be closed first;
40
+ + // the finally clause reuses the startup path to rebuild every cache.
40
41
  + return await this.enqueueOperation(async () => {
41
- + const domain = this.domain
42
- + if (domain === undefined) throw new Error('workspace registry is not started yet')
42
+ + // The queue slot runs recoverPendingMutation first, which requires the
43
+ + // registry state, so the open domain is present by this point.
44
+ + const domain = this.domain as Domain<typeof workspaceDomainSpec>
43
45
  + const previousArchivedSessions = this.requireState().archivedSessionIds.length
44
46
  + await domain.close()
45
47
  + delete this.domain
@@ -72,3 +74,113 @@ index 037344fce4..7b6d915807 100644
72
74
  this.table = domain.table('workspaces')
73
75
  this.global = domain.global
74
76
  this.state = domain.global.get()
77
+ diff --git a/packages/workspace/workspace/tests/workspace.spec.ts b/packages/workspace/workspace/tests/workspace.spec.ts
78
+ index ef3116b3d7..67721e6474 100644
79
+ --- a/packages/workspace/workspace/tests/workspace.spec.ts
80
+ +++ b/packages/workspace/workspace/tests/workspace.spec.ts
81
+ @@ -1378,6 +1378,105 @@ describe('first-use Workspace preparation', () => {
82
+ })
83
+ })
84
+
85
+ +describe('WorkspaceRegistry storage restore', () => {
86
+ + const contexts: Context[] = []
87
+ +
88
+ + afterEach(async () => {
89
+ + for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
90
+ + })
91
+ +
92
+ + it('serves the replaced baseline, not the pre-restore window, once the restore returns', async () => {
93
+ + const kept = await makeDir('restore-kept')
94
+ + const restored = await makeDir('restore-kept-2')
95
+ + const h = await harness({ sessions: [header('kept', kept, 100)] })
96
+ + contexts.push(h.ctx)
97
+ + const created = await h.registry.create(kept)
98
+ + expect(h.registry.list().map(workspace => workspace.id)).toEqual([created.id])
99
+ +
100
+ + // A restore replaces the durable medium wholesale: the registry must
101
+ + // observe the replacement only after reopening, never the old window.
102
+ + const id = WorkspaceId('restored-workspace')
103
+ + const replaced = await h.registry.withStorageRestore(async () => {
104
+ + const medium = h.pool.media.get('workspace')!
105
+ + medium.tables.set('workspaces', new Map([[id, {
106
+ + path: restored,
107
+ + title: 'restored',
108
+ + sessionIds: [],
109
+ + createdAt: '1970-01-01T00:00:01.000Z',
110
+ + updatedAt: '1970-01-01T00:00:01.000Z',
111
+ + }]]))
112
+ + medium.global = {
113
+ + initialized: true,
114
+ + workspaceIds: [id],
115
+ + archivedSessionIds: [SessionId('archived-before-restore')],
116
+ + pinnedSessionIds: [],
117
+ + }
118
+ + return 'replaced'
119
+ + })
120
+ +
121
+ + expect(replaced).toBe('replaced')
122
+ + // Both the record set and the registry-global archive set come from the
123
+ + // reopened domain; the pre-restore workspace and archive set are gone.
124
+ + expect(h.registry.list().map(workspace => workspace.path)).toEqual([restored])
125
+ + expect(h.registry.archivedSessionIds).toEqual([SessionId('archived-before-restore')])
126
+ + expect(h.registry.list().map(workspace => workspace.id)).not.toContain(created.id)
127
+ + })
128
+ +
129
+ + it('reopens through startup bootstrap so a restored empty baseline is rebuilt', async () => {
130
+ + const dir = await makeDir('restore-rebuild')
131
+ + const h = await harness({ sessions: [header('rebuild', dir, 100)] })
132
+ + contexts.push(h.ctx)
133
+ + await h.registry.create(dir)
134
+ + expect(h.registry.list()).toHaveLength(1)
135
+ +
136
+ + await h.registry.withStorageRestore(async () => {
137
+ + const medium = h.pool.media.get('workspace')!
138
+ + medium.tables.set('workspaces', new Map())
139
+ + medium.global = null
140
+ + })
141
+ +
142
+ + // The restored medium carries no initialized marker, so the reopened
143
+ + // registry bootstraps the header history again instead of serving a stale cache.
144
+ + expect(storedState(h.pool).initialized).toBe(true)
145
+ + expect(h.registry.list()).toHaveLength(1)
146
+ + })
147
+ +
148
+ + it('rebuilds every cache on the restored baseline after the replacement throws', async () => {
149
+ + const dir = await makeDir('restore-throws')
150
+ + const h = await harness({ sessions: [header('throws', dir, 100)] })
151
+ + contexts.push(h.ctx)
152
+ + await h.registry.create(dir)
153
+ +
154
+ + await expect(h.registry.withStorageRestore(async () => {
155
+ + const medium = h.pool.media.get('workspace')!
156
+ + medium.tables.set('workspaces', new Map())
157
+ + medium.global = null
158
+ + throw new Error('replacement failed')
159
+ + })).rejects.toThrow('replacement failed')
160
+ +
161
+ + // The reopen runs in the finally arm, so the registry is still usable:
162
+ + // the restored empty medium is bootstrapped from the Session headers again.
163
+ + expect(h.registry.list().map(workspace => workspace.path)).toEqual([dir])
164
+ + await expect(h.registry.create(dir)).resolves.toBeDefined()
165
+ + })
166
+ +
167
+ + it('serializes a queued mutation behind the restore it was issued during', async () => {
168
+ + const dir = await makeDir('restore-queued')
169
+ + const h = await harness({ sessions: [header('queued', dir, 100)] })
170
+ + contexts.push(h.ctx)
171
+ + const created = await h.registry.create(dir)
172
+ + const order: string[] = []
173
+ + // The restore takes the registry queue slot synchronously, so the delete
174
+ + // issued next runs only after the domain was closed and reopened.
175
+ + const restoring = h.registry.withStorageRestore(async () => { order.push('restore') })
176
+ + const deleting = h.registry.delete(created.id).then((ok) => { order.push('delete'); return ok })
177
+ + await Promise.all([restoring, deleting])
178
+ + expect(order).toEqual(['restore', 'delete'])
179
+ + expect(await deleting).toBe(true)
180
+ + expect(h.registry.list()).toEqual([])
181
+ + })
182
+ +})
183
+ +
184
+ // The registry knows no family: the providers merge theirs, and this suite merges its own.
185
+ declare module '@deepseek-ai/dsh-workspace/types' {
186
+ interface SessionActivityKindMap {