@chatcode/chatcode-cli 3.0.4-rc.1 → 3.0.4-rc.3
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.i18n.yaml +2 -2
- package/README.md +2 -0
- package/README.zh.md +2 -0
- package/lib/{command-errors-BkCHTQHj.js → command-errors-BJ_ZWzUU.js} +12 -12
- package/lib/{command-trees-5HFyC5sI.js → command-trees-C9b7-bci.js} +2 -2
- package/lib/{component-identity-De91VHwW.js → component-identity-Jb1DYjke.js} +3 -3
- package/lib/{dist-CsDWUvQ4.js → dist-w_sIm4VD.js} +231 -231
- package/lib/{extension-events-CdYCRP0o.js → extension-events-DKX4LXOd.js} +26 -26
- package/lib/{frames-DOX-Jgk8.js → frames-B89znn_0.js} +4 -4
- package/lib/{host-access-Bk4xps5E.js → host-access-CMg6rsWt.js} +1 -1
- package/lib/{keymap-CJ1kvLR2.js → keymap-CrC4pSNH.js} +2 -2
- package/lib/{plugin-XR7ncbnW.js → plugin-CmyMCGfb.js} +1651 -1365
- package/lib/{scenes-CAmWncZD.js → scenes-Cil9RnwU.js} +3 -3
- package/lib/{settings-sections-CakcvBv9.js → settings-sections-jVCqXwdH.js} +3 -3
- package/lib/{statusBarSettings-BPPS_Usm.js → statusBarSettings-DNoBH3Cq.js} +3 -3
- package/lib/{supports-color-DyChQwlI.js → supports-color-CcW-FGjr.js} +2 -2
- package/lib/{themes-OjcUpZzw.js → themes-BdgeY3It.js} +12 -12
- package/lib/tui-compat-command-trees.js +1 -1
- package/lib/tui-compat-extensions.js +3 -3
- package/lib/tui-compat-plugin-host.js +5 -5
- package/lib/tui-compat-scenes.js +1 -1
- package/lib/tui-compat-settings-sections.js +1 -1
- package/lib/tui-compat-working-activity.js +4 -4
- package/lib/tui-compat-workspaces.js +1 -1
- package/lib/tui-compat.js +5 -5
- package/lib/{workspaces-XY7UdfXp.js → workspaces-DOatJRYr.js} +2 -2
- package/package.json +30 -29
- package/runtime/manifest.json +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-agent-default-model/lib/index.js +118 -22
- package/runtime/node_modules/@deepseek-ai/dsh-agent-default-model/lib/types/index.d.ts +20 -3
- package/runtime/node_modules/@deepseek-ai/dsh-api-remotes/lib/client.js +113 -39
- package/runtime/node_modules/@deepseek-ai/dsh-api-remotes/lib/index.js +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-remotes/lib/types/remote-events.d.ts +3 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-remotes/lib/types/remote-events.js +1 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js +173 -34
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.host.js +48 -28
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.js +24 -19
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.d.ts +9 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.js +36 -6
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/archived-session-gate.js +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/catalog.js +4 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.js +24 -8
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.d.ts +4 -3
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.js +31 -14
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/list.d.ts +6 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/list.js +89 -4
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/remote-events.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts +18 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/client.js +30 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/index.js +83 -2
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/typert.host.js +104 -11
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/typert.remote-client.d.ts +5 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/typert.remote-client.js +78 -11
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/model.d.ts +14 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/model.js +17 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/service.d.ts +15 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/service.js +12 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/commands.d.ts +13 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/commands.js +44 -2
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/index.d.ts +13 -1
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/index.js +22 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/types.d.ts +17 -0
- package/runtime/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js +167 -44
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/index.d.ts +14 -6
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/linux-lock.d.ts +18 -0
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/windows-lock.d.ts +7 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/client.js +931 -7
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/index.js +57 -19
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/types/client/index.d.ts +5 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/types/config.d.ts +12 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/types/events.d.ts +4 -4
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/lib/types/index.d.ts +3 -8
- package/runtime/node_modules/@deepseek-ai/dsh-client-hmr/package.json +6 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-commands/lib/client.js +10 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/types/client/present-open.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js +8 -4
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-account/lib/client.js +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-general/lib/client.js +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js +1917 -206
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/ArchivedSessions.d.ts +13 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/contract/slots.d.ts +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/locales.d.ts +40 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/recent.d.ts +11 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/rows/RecentList.d.ts +15 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/rows/Rows.d.ts +6 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/session-actions/DeleteSession.d.ts +24 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/stores.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/tree.d.ts +15 -13
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/package.json +3 -1
- package/runtime/node_modules/@deepseek-ai/dsh-commands/lib/index.js +16 -0
- package/runtime/node_modules/@deepseek-ai/dsh-commands/lib/typert.host.js +7 -3
- package/runtime/node_modules/@deepseek-ai/dsh-commands/lib/typert.remote-client.js +6 -2
- package/runtime/node_modules/@deepseek-ai/dsh-commands/lib/types/index.js +20 -0
- package/runtime/node_modules/@deepseek-ai/dsh-commands/lib/types/types.d.ts +5 -0
- package/runtime/node_modules/@deepseek-ai/dsh-cordis-client-runner/lib/client.js +5 -2
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/adapter.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/config.d.ts +3 -3
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek-account/lib/types/config.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek-api-key/lib/types/config.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-schedule/lib/index.js +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-schedule/lib/typert.host.js +5 -5
- package/runtime/node_modules/@deepseek-ai/dsh-schedule/lib/typert.remote-client.js +5 -5
- package/runtime/node_modules/@deepseek-ai/dsh-schedule/lib/types/index.js +5 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session/lib/types/index.d.ts +21 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-persistence/lib/index.js +12 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-persistence/lib/types/index.d.ts +15 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js +95 -2
- package/runtime/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/types/index.d.ts +3 -1
- package/runtime/node_modules/@deepseek-ai/dsh-session-projection-cache/lib/index.js +25 -1
- package/runtime/node_modules/@deepseek-ai/dsh-session-projection-cache/lib/types/index.d.ts +1 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-projection-cache/package.json +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-session-query/lib/index.js +13 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-query/lib/types/observation.d.ts +5 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-query-sqlite/lib/index.js +26 -0
- package/runtime/node_modules/@deepseek-ai/dsh-session-query-sqlite/package.json +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js +5 -1
- package/runtime/node_modules/@deepseek-ai/dsh-storage-domain/lib/types/domain.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-subagent/lib/types/control.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/lib/index.js +106 -56
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/lib/types/config.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/lib/types/index.d.ts +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/lib/types/services/tianti/config.d.ts +4 -2
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/lib/types/tools.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-tianti/package.json +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-tool-cordis/lib/types/api-catalog.js +104 -13
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/index.js +215 -4
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/invariant.js +6 -1
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/index.d.ts +31 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/index.js +219 -2
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/spec.d.ts +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/spec.js +4 -0
- package/scripts/launch.mjs +8 -0
package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/service.d.ts
CHANGED
|
@@ -85,6 +85,19 @@ export interface IWorkspaces {
|
|
|
85
85
|
* @param sessionId - Session to unarchive.
|
|
86
86
|
*/
|
|
87
87
|
unarchiveSession(sessionId: SessionId): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Preview the archived root and owned subagent identities before confirmation.
|
|
90
|
+
* @param sessionId - archived root identity.
|
|
91
|
+
* @returns the identities covered by confirmation.
|
|
92
|
+
*/
|
|
93
|
+
sessionDeletionScope(sessionId: SessionId): Promise<readonly SessionId[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Permanently delete exactly the confirmed scope; live or changed scopes reject.
|
|
96
|
+
* @param sessionId - archived root identity.
|
|
97
|
+
* @param expectedSessionIds - identities confirmed by the user.
|
|
98
|
+
* @returns the identities removed after durable cleanup.
|
|
99
|
+
*/
|
|
100
|
+
deleteSession(sessionId: SessionId, expectedSessionIds: readonly SessionId[]): Promise<readonly SessionId[]>;
|
|
88
101
|
/**
|
|
89
102
|
* Pin a Session ahead of unpinned Sessions on Workspace grouping surfaces.
|
|
90
103
|
* @param sessionId - Session to pin.
|
|
@@ -124,6 +137,8 @@ export declare class WorkspaceController extends Service implements IWorkspaces
|
|
|
124
137
|
readonly stopActivity?: boolean;
|
|
125
138
|
}): Promise<void>;
|
|
126
139
|
unarchiveSession(sessionId: SessionId): Promise<void>;
|
|
140
|
+
sessionDeletionScope(sessionId: SessionId): Promise<readonly SessionId[]>;
|
|
141
|
+
deleteSession(sessionId: SessionId, expectedSessionIds: readonly SessionId[]): Promise<readonly SessionId[]>;
|
|
127
142
|
pinSession(sessionId: SessionId): Promise<void>;
|
|
128
143
|
unpinSession(sessionId: SessionId): Promise<void>;
|
|
129
144
|
insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise<WorkspaceView>;
|
package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/client/service.js
CHANGED
|
@@ -75,6 +75,18 @@ export class WorkspaceController extends Service {
|
|
|
75
75
|
if (!result.ok)
|
|
76
76
|
throw commandError('session unarchive', result.error);
|
|
77
77
|
}
|
|
78
|
+
async sessionDeletionScope(sessionId) {
|
|
79
|
+
const result = await this.model.sessionDeletionScope(sessionId);
|
|
80
|
+
if (!result.ok)
|
|
81
|
+
throw new WorkspaceArchiveError(result.error);
|
|
82
|
+
return result.value.sessionIds;
|
|
83
|
+
}
|
|
84
|
+
async deleteSession(sessionId, expectedSessionIds) {
|
|
85
|
+
const result = await this.model.deleteSession(sessionId, expectedSessionIds);
|
|
86
|
+
if (!result.ok)
|
|
87
|
+
throw new WorkspaceArchiveError(result.error);
|
|
88
|
+
return result.value.sessionIds;
|
|
89
|
+
}
|
|
78
90
|
async pinSession(sessionId) {
|
|
79
91
|
const result = await this.model.pinSession(sessionId);
|
|
80
92
|
if (!result.ok)
|
package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/commands.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Workspace command implementation and stable Remote failure mapping. */
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
-
import type { WorkspaceArchiveSessionRequest, WorkspaceArchiveValue, WorkspaceCreateRequest, WorkspaceCreateValue, WorkspaceDeleteRequest, WorkspaceDeleteValue, WorkspaceInsertBeforeRequest, WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspacePinSessionRequest, WorkspacePinValue, WorkspaceRenameRequest, WorkspaceUnarchiveSessionRequest, WorkspaceUnpinSessionRequest, WorkspaceValue } from './types.ts';
|
|
3
|
+
import type { WorkspaceArchiveSessionRequest, WorkspaceArchiveValue, WorkspaceCreateRequest, WorkspaceCreateValue, WorkspaceDeleteRequest, WorkspaceDeleteValue, WorkspaceInsertBeforeRequest, WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspacePinSessionRequest, WorkspacePinValue, WorkspaceRenameRequest, WorkspaceUnarchiveSessionRequest, WorkspaceUnpinSessionRequest, WorkspaceValue, WorkspaceSessionDeletionPreviewRequest, WorkspaceSessionDeletionRequest, WorkspaceSessionDeletionValue } from './types.ts';
|
|
4
4
|
/** Implements Workspace mutations against the authoritative registry. */
|
|
5
5
|
export declare class WorkspaceCommands {
|
|
6
6
|
private readonly ctx;
|
|
@@ -54,6 +54,18 @@ export declare class WorkspaceCommands {
|
|
|
54
54
|
* @returns the complete resulting archive set.
|
|
55
55
|
*/
|
|
56
56
|
unarchiveSession(request: WorkspaceUnarchiveSessionRequest): Promise<WorkspaceArchiveValue>;
|
|
57
|
+
/**
|
|
58
|
+
* Preview the Session identities that would be permanently deleted.
|
|
59
|
+
* @param request - archived root to preview.
|
|
60
|
+
* @returns the complete deletion scope for confirmation.
|
|
61
|
+
*/
|
|
62
|
+
sessionDeletionScope(request: WorkspaceSessionDeletionPreviewRequest): Promise<WorkspaceSessionDeletionValue>;
|
|
63
|
+
/**
|
|
64
|
+
* Delete an archived root and exactly its confirmed owned subagent scope.
|
|
65
|
+
* @param request - root and user-confirmed deletion scope.
|
|
66
|
+
* @returns identities removed after persistence and owner cleanup complete.
|
|
67
|
+
*/
|
|
68
|
+
deleteSession(request: WorkspaceSessionDeletionRequest): Promise<WorkspaceSessionDeletionValue>;
|
|
57
69
|
/**
|
|
58
70
|
* Add one known unarchived Session to the registry-global pin set.
|
|
59
71
|
* @param request - Session identity to pin.
|
package/runtime/node_modules/@deepseek-ai/dsh-api-workspace-controller/lib/types/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Workspace command implementation and stable Remote failure mapping. */
|
|
2
|
-
import { WorkspaceActiveSessionError, WorkspaceArchivedSessionPinError, WorkspaceId, WorkspaceMoveInvalidError, WorkspaceOrderInvalidError, WorkspaceUnknownSessionError, } from '@deepseek-ai/dsh-workspace';
|
|
2
|
+
import { WorkspaceActiveSessionError, WorkspaceArchivedSessionPinError, WorkspaceId, WorkspaceMoveInvalidError, WorkspaceOrderInvalidError, WorkspaceUnknownSessionError, WorkspaceSessionDeletionError, } from '@deepseek-ai/dsh-workspace';
|
|
3
3
|
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol';
|
|
4
4
|
import { workspaceView } from "./feed.js";
|
|
5
5
|
/** Implements Workspace mutations against the authoritative registry. */
|
|
@@ -138,9 +138,40 @@ export class WorkspaceCommands {
|
|
|
138
138
|
* @returns the complete resulting archive set.
|
|
139
139
|
*/
|
|
140
140
|
async unarchiveSession(request) {
|
|
141
|
-
|
|
141
|
+
try {
|
|
142
|
+
await this.ctx.workspaceRegistry.unarchiveSession(request.sessionId);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
throw deletionError(error, request.sessionId);
|
|
146
|
+
}
|
|
142
147
|
return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] };
|
|
143
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Preview the Session identities that would be permanently deleted.
|
|
151
|
+
* @param request - archived root to preview.
|
|
152
|
+
* @returns the complete deletion scope for confirmation.
|
|
153
|
+
*/
|
|
154
|
+
async sessionDeletionScope(request) {
|
|
155
|
+
try {
|
|
156
|
+
return { sessionIds: await this.ctx.workspaceRegistry.sessionDeletionScope(request.sessionId) };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw deletionError(error, request.sessionId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Delete an archived root and exactly its confirmed owned subagent scope.
|
|
164
|
+
* @param request - root and user-confirmed deletion scope.
|
|
165
|
+
* @returns identities removed after persistence and owner cleanup complete.
|
|
166
|
+
*/
|
|
167
|
+
async deleteSession(request) {
|
|
168
|
+
try {
|
|
169
|
+
return { sessionIds: await this.ctx.workspaceRegistry.deleteSession(request.sessionId, request.expectedSessionIds) };
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw deletionError(error, request.sessionId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
144
175
|
/**
|
|
145
176
|
* Add one known unarchived Session to the registry-global pin set.
|
|
146
177
|
* @param request - Session identity to pin.
|
|
@@ -187,6 +218,17 @@ export class WorkspaceCommands {
|
|
|
187
218
|
function workspaceNotFound(workspaceId) {
|
|
188
219
|
return new RemoteError('workspace/not-found', `Workspace "${workspaceId}" not found`, { workspaceId });
|
|
189
220
|
}
|
|
221
|
+
function deletionError(error, sessionId) {
|
|
222
|
+
if (error instanceof WorkspaceSessionDeletionError) {
|
|
223
|
+
return new RemoteError('workspace/session-delete-refused', 'Session deletion refused', {
|
|
224
|
+
sessionId, reason: error.reason,
|
|
225
|
+
}, { cause: error });
|
|
226
|
+
}
|
|
227
|
+
if (error instanceof WorkspaceActiveSessionError) {
|
|
228
|
+
return new RemoteError('workspace/session-active', error.message, { sessionId, activity: error.activity }, { cause: error });
|
|
229
|
+
}
|
|
230
|
+
return error;
|
|
231
|
+
}
|
|
190
232
|
function errorMessage(error) {
|
|
191
233
|
return error instanceof Error ? error.message : String(error);
|
|
192
234
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
4
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
5
|
-
import type { WorkspaceArchiveSessionRequest, WorkspaceArchiveValue, WorkspaceCreateRequest, WorkspaceCreateValue, WorkspaceDeleteRequest, WorkspaceDeleteValue, WorkspaceFollowFrame, WorkspaceInsertBeforeRequest, WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspacePinSessionRequest, WorkspacePinValue, WorkspaceRenameRequest, WorkspaceUnarchiveSessionRequest, WorkspaceUnpinSessionRequest, WorkspaceValue } from './types.ts';
|
|
5
|
+
import type { WorkspaceArchiveSessionRequest, WorkspaceArchiveValue, WorkspaceCreateRequest, WorkspaceCreateValue, WorkspaceDeleteRequest, WorkspaceDeleteValue, WorkspaceFollowFrame, WorkspaceInsertBeforeRequest, WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspacePinSessionRequest, WorkspacePinValue, WorkspaceRenameRequest, WorkspaceUnarchiveSessionRequest, WorkspaceSessionDeletionPreviewRequest, WorkspaceSessionDeletionRequest, WorkspaceSessionDeletionValue, WorkspaceUnpinSessionRequest, WorkspaceValue } from './types.ts';
|
|
6
6
|
export type * from './types.ts';
|
|
7
7
|
export { DirectoryPickerController } from './directory-picker.ts';
|
|
8
8
|
/** First-use directory policy for the Host account. */
|
|
@@ -85,6 +85,18 @@ export declare class WorkspaceController extends TypertRemoteService {
|
|
|
85
85
|
* @returns the complete resulting archive set.
|
|
86
86
|
*/
|
|
87
87
|
unarchiveSession(request: WorkspaceUnarchiveSessionRequest): Promise<WorkspaceArchiveValue>;
|
|
88
|
+
/**
|
|
89
|
+
* Preview the archived root and owned subagent Sessions covered by deletion.
|
|
90
|
+
* @param request - archived root to preview.
|
|
91
|
+
* @returns complete deletion scope for confirmation.
|
|
92
|
+
*/
|
|
93
|
+
sessionDeletionScope(request: WorkspaceSessionDeletionPreviewRequest): Promise<WorkspaceSessionDeletionValue>;
|
|
94
|
+
/**
|
|
95
|
+
* Permanently remove an archived Session and its confirmed owned subagents.
|
|
96
|
+
* @param request - archived root and confirmed scope.
|
|
97
|
+
* @returns identities permanently removed.
|
|
98
|
+
*/
|
|
99
|
+
deleteSession(request: WorkspaceSessionDeletionRequest): Promise<WorkspaceSessionDeletionValue>;
|
|
88
100
|
/**
|
|
89
101
|
* Surface one known unarchived Session ahead of unpinned Sessions.
|
|
90
102
|
* @param request - Session identity to pin.
|
|
@@ -52,6 +52,8 @@ let WorkspaceController = (() => {
|
|
|
52
52
|
let _insertSessionBefore_decorators;
|
|
53
53
|
let _archiveSession_decorators;
|
|
54
54
|
let _unarchiveSession_decorators;
|
|
55
|
+
let _sessionDeletionScope_decorators;
|
|
56
|
+
let _deleteSession_decorators;
|
|
55
57
|
let _pinSession_decorators;
|
|
56
58
|
let _unpinSession_decorators;
|
|
57
59
|
let _follow_decorators;
|
|
@@ -66,6 +68,8 @@ let WorkspaceController = (() => {
|
|
|
66
68
|
_insertSessionBefore_decorators = [Remote('insertSessionBefore')];
|
|
67
69
|
_archiveSession_decorators = [Remote('archiveSession')];
|
|
68
70
|
_unarchiveSession_decorators = [Remote('unarchiveSession')];
|
|
71
|
+
_sessionDeletionScope_decorators = [Remote('sessionDeletionScope')];
|
|
72
|
+
_deleteSession_decorators = [Remote('deleteSession')];
|
|
69
73
|
_pinSession_decorators = [Remote('pinSession')];
|
|
70
74
|
_unpinSession_decorators = [Remote('unpinSession')];
|
|
71
75
|
_follow_decorators = [Remote({ mode: 'stream' })];
|
|
@@ -77,6 +81,8 @@ let WorkspaceController = (() => {
|
|
|
77
81
|
__esDecorate(this, null, _insertSessionBefore_decorators, { kind: "method", name: "insertSessionBefore", static: false, private: false, access: { has: obj => "insertSessionBefore" in obj, get: obj => obj.insertSessionBefore }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
78
82
|
__esDecorate(this, null, _archiveSession_decorators, { kind: "method", name: "archiveSession", static: false, private: false, access: { has: obj => "archiveSession" in obj, get: obj => obj.archiveSession }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
79
83
|
__esDecorate(this, null, _unarchiveSession_decorators, { kind: "method", name: "unarchiveSession", static: false, private: false, access: { has: obj => "unarchiveSession" in obj, get: obj => obj.unarchiveSession }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
84
|
+
__esDecorate(this, null, _sessionDeletionScope_decorators, { kind: "method", name: "sessionDeletionScope", static: false, private: false, access: { has: obj => "sessionDeletionScope" in obj, get: obj => obj.sessionDeletionScope }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
85
|
+
__esDecorate(this, null, _deleteSession_decorators, { kind: "method", name: "deleteSession", static: false, private: false, access: { has: obj => "deleteSession" in obj, get: obj => obj.deleteSession }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
80
86
|
__esDecorate(this, null, _pinSession_decorators, { kind: "method", name: "pinSession", static: false, private: false, access: { has: obj => "pinSession" in obj, get: obj => obj.pinSession }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
81
87
|
__esDecorate(this, null, _unpinSession_decorators, { kind: "method", name: "unpinSession", static: false, private: false, access: { has: obj => "unpinSession" in obj, get: obj => obj.unpinSession }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
82
88
|
__esDecorate(this, null, _follow_decorators, { kind: "method", name: "follow", static: false, private: false, access: { has: obj => "follow" in obj, get: obj => obj.follow }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -178,6 +184,22 @@ let WorkspaceController = (() => {
|
|
|
178
184
|
unarchiveSession(request) {
|
|
179
185
|
return this.commands.unarchiveSession(request);
|
|
180
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Preview the archived root and owned subagent Sessions covered by deletion.
|
|
189
|
+
* @param request - archived root to preview.
|
|
190
|
+
* @returns complete deletion scope for confirmation.
|
|
191
|
+
*/
|
|
192
|
+
sessionDeletionScope(request) {
|
|
193
|
+
return this.commands.sessionDeletionScope(request);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Permanently remove an archived Session and its confirmed owned subagents.
|
|
197
|
+
* @param request - archived root and confirmed scope.
|
|
198
|
+
* @returns identities permanently removed.
|
|
199
|
+
*/
|
|
200
|
+
deleteSession(request) {
|
|
201
|
+
return this.commands.deleteSession(request);
|
|
202
|
+
}
|
|
181
203
|
/**
|
|
182
204
|
* Surface one known unarchived Session ahead of unpinned Sessions.
|
|
183
205
|
* @param request - Session identity to pin.
|
|
@@ -42,6 +42,11 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
42
42
|
readonly sessionId: SessionId;
|
|
43
43
|
readonly activity: readonly SessionActivity[];
|
|
44
44
|
};
|
|
45
|
+
/** Permanent deletion was refused before storage mutation. */
|
|
46
|
+
'workspace/session-delete-refused': {
|
|
47
|
+
readonly sessionId: SessionId;
|
|
48
|
+
readonly reason: 'not-archived' | 'in-use' | 'deleted' | 'scope-changed';
|
|
49
|
+
};
|
|
45
50
|
/** The Session or its anchor is not in the Workspace's manual order. */
|
|
46
51
|
'workspace/move-invalid': {
|
|
47
52
|
readonly workspaceId: WorkspaceId;
|
|
@@ -123,6 +128,18 @@ export interface WorkspaceArchiveSessionRequest {
|
|
|
123
128
|
export interface WorkspaceUnarchiveSessionRequest {
|
|
124
129
|
readonly sessionId: SessionId;
|
|
125
130
|
}
|
|
131
|
+
/** Archived root whose deletion scope is requested. */
|
|
132
|
+
export interface WorkspaceSessionDeletionPreviewRequest {
|
|
133
|
+
readonly sessionId: SessionId;
|
|
134
|
+
}
|
|
135
|
+
/** Archived root and the exact scope confirmed by the user. */
|
|
136
|
+
export interface WorkspaceSessionDeletionRequest extends WorkspaceSessionDeletionPreviewRequest {
|
|
137
|
+
readonly expectedSessionIds: readonly SessionId[];
|
|
138
|
+
}
|
|
139
|
+
/** Root and owned subagent identities; independent forks are excluded. */
|
|
140
|
+
export interface WorkspaceSessionDeletionValue {
|
|
141
|
+
readonly sessionIds: readonly SessionId[];
|
|
142
|
+
}
|
|
126
143
|
/** Complete archived Session set after a mutation. */
|
|
127
144
|
export interface WorkspaceArchiveValue {
|
|
128
145
|
readonly archivedSessionIds: readonly SessionId[];
|
|
@@ -3922,7 +3922,8 @@ const requiredStartupEntryIds = new Set([
|
|
|
3922
3922
|
"connection",
|
|
3923
3923
|
"headless-runner",
|
|
3924
3924
|
"acp",
|
|
3925
|
-
"sdk-jsonrpc-server"
|
|
3925
|
+
"sdk-jsonrpc-server",
|
|
3926
|
+
"dsh-tui"
|
|
3926
3927
|
]);
|
|
3927
3928
|
/** Render plugin stacks, nested causes, and aggregate member failures once per error. */
|
|
3928
3929
|
function formatActivationError(error) {
|
|
@@ -1,6 +1,122 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
2
|
+
import { link, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, win32 } from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
//#region lib/types/windows-lock.js
|
|
7
|
+
/** Exclusive Windows inspection for an empty or zero-filled lock left by a failed creator. */
|
|
8
|
+
const execute = promisify(execFile);
|
|
9
|
+
/**
|
|
10
|
+
* Read a small lock only when no other handle has the file open.
|
|
11
|
+
* @param path - lock path, protected by the caller's takeover claim.
|
|
12
|
+
* @returns its record, or undefined if exclusive access or inspection fails.
|
|
13
|
+
*/
|
|
14
|
+
async function readUnopenedWindowsLock(path) {
|
|
15
|
+
const powershell = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
16
|
+
const script = [
|
|
17
|
+
"$ErrorActionPreference = 'Stop'",
|
|
18
|
+
`$file = [IO.File]::Open('${path.replaceAll("'", "''")}', [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)`,
|
|
19
|
+
"try {",
|
|
20
|
+
" if ($file.Length -gt 64) { exit 1 }",
|
|
21
|
+
" $reader = [IO.StreamReader]::new($file, [Text.Encoding]::UTF8, $false)",
|
|
22
|
+
" try { [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($reader.ReadToEnd()))) }",
|
|
23
|
+
" finally { $reader.Dispose() }",
|
|
24
|
+
"} finally { $file.Dispose() }"
|
|
25
|
+
].join("\n");
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execute(powershell, [
|
|
28
|
+
"-NoLogo",
|
|
29
|
+
"-NoProfile",
|
|
30
|
+
"-NonInteractive",
|
|
31
|
+
"-EncodedCommand",
|
|
32
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
33
|
+
], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
timeout: 2e3,
|
|
37
|
+
maxBuffer: 4096,
|
|
38
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([name]) => !/KEY|SECRET|TOKEN|PASSWORD/i.test(name)))
|
|
39
|
+
});
|
|
40
|
+
const value = stdout.trim();
|
|
41
|
+
const bytes = Buffer.from(value, "base64");
|
|
42
|
+
if (bytes.length > 64 || bytes.toString("base64") !== value) return void 0;
|
|
43
|
+
return bytes.toString("utf8");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region lib/types/linux-lock.js
|
|
50
|
+
/** Linux writer identity and complete-record publication; other platforms retain PID records. */
|
|
51
|
+
const RECORD = /^(\d+)\r?\n(?:linux ([a-f0-9-]{36}) (\d+) ([a-f0-9]{24})\n)?$/u;
|
|
52
|
+
let ownIdentity;
|
|
53
|
+
/** Read kernel identity without treating inaccessible procfs as evidence of an exited writer. */
|
|
54
|
+
async function identity(pid) {
|
|
55
|
+
try {
|
|
56
|
+
const [boot, stat] = await Promise.all([readFile("/proc/sys/kernel/random/boot_id", "utf8"), readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
57
|
+
const end = stat.lastIndexOf(")");
|
|
58
|
+
const start = end < 0 ? void 0 : stat.slice(end + 2).trim().split(/\s+/u)[19];
|
|
59
|
+
if (!/^[a-f0-9-]{36}$/u.test(boot.trim()) || start === void 0 || !/^\d+$/u.test(start)) return void 0;
|
|
60
|
+
return `${boot.trim()} ${start}`;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render a new acquisition's record; Linux adds boot identity, start ticks, and a unique claim token.
|
|
67
|
+
* @returns the PID record, with Linux identity when the kernel exposes it.
|
|
68
|
+
*/
|
|
69
|
+
async function writerLockRecord() {
|
|
70
|
+
const pid = `${process.pid}\n`;
|
|
71
|
+
if (process.platform !== "linux") return pid;
|
|
72
|
+
const current = await (ownIdentity ??= identity(process.pid));
|
|
73
|
+
return current === void 0 ? pid : `${pid}linux ${current} ${randomBytes(12).toString("hex")}\n`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Exclusively publish a complete Linux identity record, or create a legacy PID lock.
|
|
77
|
+
* @param path - lock sibling in an existing parent directory.
|
|
78
|
+
* @param record - one acquisition's record from writerLockRecord.
|
|
79
|
+
*/
|
|
80
|
+
async function createWriterLock(path, record) {
|
|
81
|
+
const nonce = RECORD.exec(record)?.[4];
|
|
82
|
+
if (process.platform !== "linux" || nonce === void 0) {
|
|
83
|
+
await writeFile(path, record, {
|
|
84
|
+
mode: 384,
|
|
85
|
+
flag: "wx"
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const temp = `${path}.${nonce}.tmp`;
|
|
90
|
+
try {
|
|
91
|
+
await writeFile(temp, record, {
|
|
92
|
+
mode: 384,
|
|
93
|
+
flag: "wx"
|
|
94
|
+
});
|
|
95
|
+
await link(temp, path);
|
|
96
|
+
} finally {
|
|
97
|
+
await rm(temp, { force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
102
|
+
* @param record - untrusted contents of an existing lock file.
|
|
103
|
+
* @returns true only for ESRCH or a different observable Linux process identity.
|
|
104
|
+
*/
|
|
105
|
+
async function writerExited(record) {
|
|
106
|
+
const match = RECORD.exec(record);
|
|
107
|
+
if (match === null) return false;
|
|
108
|
+
const pid = Number(match[1]);
|
|
109
|
+
if (pid === 0 || pid > 2147483647) return false;
|
|
110
|
+
if (pid !== process.pid) try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return error.code === "ESRCH";
|
|
114
|
+
}
|
|
115
|
+
if (process.platform !== "linux" || match[2] === void 0) return false;
|
|
116
|
+
const current = await identity(pid);
|
|
117
|
+
return current !== void 0 && current !== `${match[2]} ${match[3]}`;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
4
120
|
//#region lib/types/index.js
|
|
5
121
|
/**
|
|
6
122
|
* Zero-dependency atomic file replacement and writer coordination.
|
|
@@ -8,38 +124,41 @@ import { dirname } from "node:path";
|
|
|
8
124
|
* the caller's permission bits, then renames it over the target, so readers
|
|
9
125
|
* observe either the old or the new complete content and a replaced file ends
|
|
10
126
|
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
11
|
-
* writers of one file through
|
|
127
|
+
* writers of one file through an exclusively published `<file>.lock` sibling, so a
|
|
12
128
|
* read-modify-write cycle can never resurrect a state another writer just
|
|
13
129
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
14
|
-
* whose recorded holder process no longer exists is taken over.
|
|
130
|
+
* whose recorded holder process no longer exists is taken over. Windows also
|
|
131
|
+
* recovers empty or zero-filled records after an exclusive read proves no
|
|
132
|
+
* creator still has the lock file open. Linux records also distinguish
|
|
133
|
+
* a previous boot or reused PID through kernel process identity.
|
|
15
134
|
* @module @deepseek-ai/dsh-atomic-write
|
|
16
135
|
*/
|
|
17
|
-
const
|
|
136
|
+
const WINDOWS_TRANSIENT_FILE_ERRORS = new Set([
|
|
18
137
|
"EACCES",
|
|
19
138
|
"EBUSY",
|
|
20
139
|
"EPERM"
|
|
21
140
|
]);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
141
|
+
const WINDOWS_FILE_RETRY_INITIAL_MS = 20;
|
|
142
|
+
const WINDOWS_FILE_RETRY_MAX_MS = 200;
|
|
143
|
+
const WINDOWS_FILE_RETRY_LIMIT = 8;
|
|
25
144
|
/** Whether Windows reported temporary interference with an atomic replacement. */
|
|
26
|
-
function
|
|
145
|
+
function isTransientWindowsFileError(error) {
|
|
27
146
|
if (process.platform !== "win32") return false;
|
|
28
|
-
return
|
|
147
|
+
return WINDOWS_TRANSIENT_FILE_ERRORS.has(error?.code ?? "");
|
|
29
148
|
}
|
|
30
|
-
/**
|
|
31
|
-
async function
|
|
32
|
-
let delay =
|
|
149
|
+
/** Retry a replacement or lock release while Windows briefly holds the path open. */
|
|
150
|
+
async function retryWindowsFileOperation(operation) {
|
|
151
|
+
let delay = WINDOWS_FILE_RETRY_INITIAL_MS;
|
|
33
152
|
for (let retries = 0;; retries += 1) {
|
|
34
153
|
try {
|
|
35
|
-
await
|
|
154
|
+
await operation();
|
|
36
155
|
return;
|
|
37
156
|
} catch (error) {
|
|
38
|
-
if (!
|
|
39
|
-
if (retries >=
|
|
157
|
+
if (!isTransientWindowsFileError(error)) throw error;
|
|
158
|
+
if (retries >= WINDOWS_FILE_RETRY_LIMIT) throw error;
|
|
40
159
|
}
|
|
41
160
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42
|
-
delay = Math.min(delay * 2,
|
|
161
|
+
delay = Math.min(delay * 2, WINDOWS_FILE_RETRY_MAX_MS);
|
|
43
162
|
}
|
|
44
163
|
}
|
|
45
164
|
/**
|
|
@@ -69,7 +188,7 @@ async function writeFileAtomic(filename, content, options) {
|
|
|
69
188
|
mode: options.mode,
|
|
70
189
|
flag: "wx"
|
|
71
190
|
});
|
|
72
|
-
await
|
|
191
|
+
await retryWindowsFileOperation(() => rename(temp, filename));
|
|
73
192
|
} catch (error) {
|
|
74
193
|
await rm(temp, { force: true });
|
|
75
194
|
throw error;
|
|
@@ -87,18 +206,13 @@ async function isLockContention(error, lockPath) {
|
|
|
87
206
|
return false;
|
|
88
207
|
}
|
|
89
208
|
}
|
|
90
|
-
/** Whether the
|
|
91
|
-
function holderExited(record) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
process.kill(pid, 0);
|
|
98
|
-
return false;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return error.code === "ESRCH";
|
|
101
|
-
}
|
|
209
|
+
/** Whether the recorded writer is absent or has a different observable Linux identity. */
|
|
210
|
+
async function holderExited(record) {
|
|
211
|
+
return writerExited(record);
|
|
212
|
+
}
|
|
213
|
+
/** A crash can leave an empty or zero-filled record before the holder's PID reaches disk. */
|
|
214
|
+
function damagedRecord(record) {
|
|
215
|
+
return record.length <= 64 && /^\0*$/.test(record);
|
|
102
216
|
}
|
|
103
217
|
/** The lock file's content, or undefined when it cannot be read. */
|
|
104
218
|
async function readLockRecord(lockPath) {
|
|
@@ -109,18 +223,21 @@ async function readLockRecord(lockPath) {
|
|
|
109
223
|
}
|
|
110
224
|
}
|
|
111
225
|
/**
|
|
112
|
-
* Remove
|
|
226
|
+
* Remove an exited holder's lock or a closed, damaged Windows lock. Contenders that read the
|
|
113
227
|
* same record serialize on a claim file named after it. Under the claim, the
|
|
114
228
|
* claimant re-reads the lock and probes its PID again, and removes it only
|
|
115
|
-
* when it still holds that record and that PID is still gone
|
|
229
|
+
* when it still holds that record and that PID is still gone. A damaged Windows
|
|
230
|
+
* record instead requires an exclusive read after claiming it. The record's
|
|
116
231
|
* holder can no longer release it, and no other contender can remove it
|
|
117
232
|
* without the claim, so a removal never deletes a lock another contender
|
|
118
233
|
* acquired after the dead holder's, including one whose holder reused the PID.
|
|
119
|
-
* @returns Whether this call removed the
|
|
234
|
+
* @returns Whether this call removed the abandoned lock.
|
|
120
235
|
*/
|
|
121
236
|
async function takeOverExitedLock(lockPath) {
|
|
122
237
|
const record = await readLockRecord(lockPath);
|
|
123
|
-
if (record === void 0
|
|
238
|
+
if (record === void 0) return false;
|
|
239
|
+
const damaged = process.platform === "win32" && damagedRecord(record);
|
|
240
|
+
if (!damaged && !await holderExited(record)) return false;
|
|
124
241
|
const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
|
|
125
242
|
try {
|
|
126
243
|
await writeFile(claim, `${process.pid}\n`, {
|
|
@@ -133,7 +250,10 @@ async function takeOverExitedLock(lockPath) {
|
|
|
133
250
|
throw error;
|
|
134
251
|
}
|
|
135
252
|
try {
|
|
136
|
-
if (await readLockRecord(lockPath) !== record
|
|
253
|
+
if (await readLockRecord(lockPath) !== record) return false;
|
|
254
|
+
if (damaged) {
|
|
255
|
+
if (await readUnopenedWindowsLock(lockPath) !== record) return false;
|
|
256
|
+
} else if (!await holderExited(record)) return false;
|
|
137
257
|
try {
|
|
138
258
|
await rm(lockPath, { force: true });
|
|
139
259
|
} catch (error) {
|
|
@@ -163,7 +283,7 @@ const LOCK_RETRY_MAX_MS = 200;
|
|
|
163
283
|
const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
164
284
|
/**
|
|
165
285
|
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
166
|
-
* lock is
|
|
286
|
+
* lock is an exclusively published sibling (`<filename>.lock`); paired with the
|
|
167
287
|
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
168
288
|
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
169
289
|
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
@@ -172,9 +292,14 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
|
172
292
|
* permission error is rethrown. The lock records its holder's PID. A contender
|
|
173
293
|
* removes the lock and retries at once when no process with that PID exists
|
|
174
294
|
* (`ESRCH`); any other lock, including one whose holder exists under another
|
|
175
|
-
* user (`EPERM`) or whose record is incomplete, is waited for.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
295
|
+
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
296
|
+
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
297
|
+
* exclusive inspection proves no creator still has the file open. A failed
|
|
298
|
+
* inspection preserves the lock. Windows lock release retries transient file
|
|
299
|
+
* interference for a bounded interval. Contention backs
|
|
300
|
+
* off exponentially and times out after the deadline. Linux identity records
|
|
301
|
+
* distinguish PID reuse and a previous boot; legacy PID-only records cannot.
|
|
302
|
+
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
178
303
|
* proves only that the recorded process exited: an operation that starts other
|
|
179
304
|
* writers must stop them with it or leave its successor a way to find them.
|
|
180
305
|
* PIDs are compared on the contender's host, so writers on other hosts or in
|
|
@@ -187,15 +312,13 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
|
187
312
|
*/
|
|
188
313
|
async function withFileLock(filename, operation, options) {
|
|
189
314
|
const lockPath = `${filename}.lock`;
|
|
315
|
+
const record = await writerLockRecord();
|
|
190
316
|
const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
|
|
191
317
|
let delay = LOCK_RETRY_INITIAL_MS;
|
|
192
318
|
let retriedUnconfirmedPermissionError = false;
|
|
193
319
|
for (;;) {
|
|
194
320
|
try {
|
|
195
|
-
await
|
|
196
|
-
mode: 384,
|
|
197
|
-
flag: "wx"
|
|
198
|
-
});
|
|
321
|
+
await createWriterLock(lockPath, record);
|
|
199
322
|
break;
|
|
200
323
|
} catch (error) {
|
|
201
324
|
if (!await isLockContention(error, lockPath)) {
|
|
@@ -210,7 +333,7 @@ async function withFileLock(filename, operation, options) {
|
|
|
210
333
|
try {
|
|
211
334
|
return await operation();
|
|
212
335
|
} finally {
|
|
213
|
-
await rm(lockPath, { force: true });
|
|
336
|
+
await retryWindowsFileOperation(() => rm(lockPath, { force: true }));
|
|
214
337
|
}
|
|
215
338
|
}
|
|
216
339
|
//#endregion
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* the caller's permission bits, then renames it over the target, so readers
|
|
5
5
|
* observe either the old or the new complete content and a replaced file ends
|
|
6
6
|
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
7
|
-
* writers of one file through
|
|
7
|
+
* writers of one file through an exclusively published `<file>.lock` sibling, so a
|
|
8
8
|
* read-modify-write cycle can never resurrect a state another writer just
|
|
9
9
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
10
|
-
* whose recorded holder process no longer exists is taken over.
|
|
10
|
+
* whose recorded holder process no longer exists is taken over. Windows also
|
|
11
|
+
* recovers empty or zero-filled records after an exclusive read proves no
|
|
12
|
+
* creator still has the lock file open. Linux records also distinguish
|
|
13
|
+
* a previous boot or reused PID through kernel process identity.
|
|
11
14
|
* @module @deepseek-ai/dsh-atomic-write
|
|
12
15
|
*/
|
|
13
16
|
/**
|
|
@@ -58,7 +61,7 @@ export interface FileLockOptions {
|
|
|
58
61
|
}
|
|
59
62
|
/**
|
|
60
63
|
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
61
|
-
* lock is
|
|
64
|
+
* lock is an exclusively published sibling (`<filename>.lock`); paired with the
|
|
62
65
|
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
63
66
|
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
64
67
|
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
@@ -67,9 +70,14 @@ export interface FileLockOptions {
|
|
|
67
70
|
* permission error is rethrown. The lock records its holder's PID. A contender
|
|
68
71
|
* removes the lock and retries at once when no process with that PID exists
|
|
69
72
|
* (`ESRCH`); any other lock, including one whose holder exists under another
|
|
70
|
-
* user (`EPERM`) or whose record is incomplete, is waited for.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
+
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
74
|
+
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
75
|
+
* exclusive inspection proves no creator still has the file open. A failed
|
|
76
|
+
* inspection preserves the lock. Windows lock release retries transient file
|
|
77
|
+
* interference for a bounded interval. Contention backs
|
|
78
|
+
* off exponentially and times out after the deadline. Linux identity records
|
|
79
|
+
* distinguish PID reuse and a previous boot; legacy PID-only records cannot.
|
|
80
|
+
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
73
81
|
* proves only that the recorded process exited: an operation that starts other
|
|
74
82
|
* writers must stop them with it or leave its successor a way to find them.
|
|
75
83
|
* PIDs are compared on the contender's host, so writers on other hosts or in
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a new acquisition's record; Linux adds boot identity, start ticks, and a unique claim token.
|
|
3
|
+
* @returns the PID record, with Linux identity when the kernel exposes it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function writerLockRecord(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Exclusively publish a complete Linux identity record, or create a legacy PID lock.
|
|
8
|
+
* @param path - lock sibling in an existing parent directory.
|
|
9
|
+
* @param record - one acquisition's record from writerLockRecord.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createWriterLock(path: string, record: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
14
|
+
* @param record - untrusted contents of an existing lock file.
|
|
15
|
+
* @returns true only for ESRCH or a different observable Linux process identity.
|
|
16
|
+
*/
|
|
17
|
+
export declare function writerExited(record: string): Promise<boolean>;
|
|
18
|
+
//# sourceMappingURL=linux-lock.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a small lock only when no other handle has the file open.
|
|
3
|
+
* @param path - lock path, protected by the caller's takeover claim.
|
|
4
|
+
* @returns its record, or undefined if exclusive access or inspection fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readUnopenedWindowsLock(path: string): Promise<string | undefined>;
|
|
7
|
+
//# sourceMappingURL=windows-lock.d.ts.map
|