dsh-over-ssh 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/agents/agent-manifest.json +1 -1
- package/lib/client.js +363 -314
- package/lib/file-references.js +80 -35
- package/lib/fs.js +158 -75
- package/lib/index.js +4103 -3397
- package/lib/terminal-wrapper.js +9 -4
- package/lib/types/src/config.d.ts +80 -9
- package/lib/types/src/file-references.d.ts +0 -1
- package/lib/types/src/host/app/backend.d.ts +9 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +0 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +0 -1
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +12 -1
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-selector.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-service.d.ts +0 -1
- package/lib/types/src/host/domain/capability.d.ts +0 -1
- package/lib/types/src/host/domain/errors.d.ts +0 -1
- package/lib/types/src/host/domain/roots/connection.d.ts +0 -1
- package/lib/types/src/host/domain/roots/root.d.ts +24 -9
- package/lib/types/src/host/domain/roots/session-roots.d.ts +0 -1
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +0 -1
- package/lib/types/src/host/domain/session/session-admin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/transfer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport-base.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport.d.ts +16 -2
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +0 -1
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +0 -1
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +0 -2
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +0 -1
- package/lib/types/src/index.d.ts +23 -1
- package/lib/types/src/local-bridge-refs.d.ts +0 -1
- package/lib/types/src/local-bridge.d.ts +0 -1
- package/lib/types/src/shared/root-keys.d.ts +120 -0
- package/lib/types/src/wiring/helpers.d.ts +0 -1
- package/package.json +5 -3
- package/lib/types/src/client/controls.d.ts +0 -131
- package/lib/types/src/client/core/card-focus-trap.d.ts +0 -45
- package/lib/types/src/client/core/controls.d.ts +0 -164
- package/lib/types/src/client/core/latest.d.ts +0 -48
- package/lib/types/src/client/core/locale.d.ts +0 -38
- package/lib/types/src/client/core/message.d.ts +0 -37
- package/lib/types/src/client/core/panel-model.d.ts +0 -134
- package/lib/types/src/client/core/portal.d.ts +0 -266
- package/lib/types/src/client/core/remote.d.ts +0 -84
- package/lib/types/src/client/core/styles.d.ts +0 -458
- package/lib/types/src/client/file-tree-io.d.ts +0 -19
- package/lib/types/src/client/file-tree.d.ts +0 -271
- package/lib/types/src/client/flows/directory-browser.d.ts +0 -587
- package/lib/types/src/client/flows/session-directory.d.ts +0 -415
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +0 -480
- package/lib/types/src/client/locale.d.ts +0 -31
- package/lib/types/src/client/panel-model.d.ts +0 -37
- package/lib/types/src/client/panel.d.ts +0 -77
- package/lib/types/src/client/roots/connection-test.d.ts +0 -69
- package/lib/types/src/client/roots/roots-panel-state.d.ts +0 -307
- package/lib/types/src/client/roots/roots-panel-view.d.ts +0 -280
- package/lib/types/src/client/roots/roots-panel.d.ts +0 -364
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +0 -35
- package/lib/types/src/client/roots/transfer-panel.d.ts +0 -241
- package/lib/types/src/client/roots/transfer-progress.d.ts +0 -67
- package/lib/types/src/client/roots-panel-view.d.ts +0 -236
- package/lib/types/src/client/roots-panel.d.ts +0 -347
- package/lib/types/src/client/session/session-panel-view.d.ts +0 -336
- package/lib/types/src/client/session/session-panel.d.ts +0 -322
- package/lib/types/src/client/session/session-remote-mark.d.ts +0 -110
- package/lib/types/src/client/session/session-row-actions.d.ts +0 -398
- package/lib/types/src/client/session-panel.d.ts +0 -152
- package/lib/types/src/client/styles.d.ts +0 -252
- package/lib/types/src/client/workspace-directory-flow.d.ts +0 -342
- package/lib/types/src/fs.d.ts +0 -46
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +0 -38
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +0 -215
- package/lib/types/src/host/app/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin-service.d.ts +0 -45
- package/lib/types/src/host/app/recycle-bin-world.d.ts +0 -35
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +0 -369
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +0 -130
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +0 -26
- package/lib/types/src/host/app/roots/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/roots/user-roots.d.ts +0 -552
- package/lib/types/src/host/app/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots-api-contract.d.ts +0 -339
- package/lib/types/src/host/app/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots-api-support.d.ts +0 -182
- package/lib/types/src/host/app/roots-api-transfers.d.ts +0 -13
- package/lib/types/src/host/app/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/routed-fs.d.ts +0 -239
- package/lib/types/src/host/app/serialize.d.ts +0 -37
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin-contract.d.ts +0 -214
- package/lib/types/src/host/app/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-service.d.ts +0 -38
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +0 -227
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +0 -55
- package/lib/types/src/host/app/transport-selector.d.ts +0 -98
- package/lib/types/src/host/app/transport-service.d.ts +0 -75
- package/lib/types/src/host/app/user-roots.d.ts +0 -461
- package/lib/types/src/host/domain/bin-location.d.ts +0 -79
- package/lib/types/src/host/domain/connection.d.ts +0 -95
- package/lib/types/src/host/domain/glob-pattern.d.ts +0 -62
- package/lib/types/src/host/domain/literal-text.d.ts +0 -63
- package/lib/types/src/host/domain/path-grammar.d.ts +0 -84
- package/lib/types/src/host/domain/recycle-bin.d.ts +0 -337
- package/lib/types/src/host/domain/root.d.ts +0 -74
- package/lib/types/src/host/domain/roots/bin-location.d.ts +0 -91
- package/lib/types/src/host/domain/roots/route-table.d.ts +0 -102
- package/lib/types/src/host/domain/route-table.d.ts +0 -80
- package/lib/types/src/host/domain/session/terminal-target.d.ts +0 -136
- package/lib/types/src/host/domain/session-admin.d.ts +0 -330
- package/lib/types/src/host/domain/shell.d.ts +0 -47
- package/lib/types/src/host/domain/ssh-config.d.ts +0 -73
- package/lib/types/src/host/domain/terminal-target.d.ts +0 -132
- package/lib/types/src/host/domain/tool-requests.d.ts +0 -369
- package/lib/types/src/host/domain/transfer.d.ts +0 -260
- package/lib/types/src/host/infra/backend/local-backend.d.ts +0 -84
- package/lib/types/src/host/infra/backend/local-version.d.ts +0 -43
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +0 -89
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +0 -239
- package/lib/types/src/host/infra/cache/lru.d.ts +0 -52
- package/lib/types/src/host/infra/cache/versioned.d.ts +0 -77
- package/lib/types/src/host/infra/temp-name.d.ts +0 -19
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +0 -132
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +0 -140
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +0 -564
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +0 -418
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +0 -121
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +0 -123
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +0 -131
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +0 -509
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +0 -353
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/command-pool.d.ts +0 -61
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +0 -140
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +0 -63
- package/lib/types/src/host/infra/transport/git-status.d.ts +0 -101
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +0 -26
- package/lib/types/src/host/infra/transport/session-window.d.ts +0 -51
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +0 -59
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +0 -62
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +0 -302
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +0 -66
- package/lib/types/src/host/integration/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent-preflight.d.ts +0 -56
- package/lib/types/src/host/integration/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/askpass-registry.d.ts +0 -59
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/file-reference-provider.d.ts +0 -115
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +0 -21
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +0 -94
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +0 -369
- package/lib/types/src/host/integration/fs/remote-index.d.ts +0 -177
- package/lib/types/src/host/integration/fs-provider.d.ts +0 -226
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +0 -38
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +0 -67
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +0 -130
- package/lib/types/src/host/integration/management-tools.d.ts +0 -64
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +0 -247
- package/lib/types/src/host/integration/remote/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/remote/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +0 -108
- package/lib/types/src/host/integration/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote-file-tools.d.ts +0 -241
- package/lib/types/src/host/integration/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/remote-index.d.ts +0 -165
- package/lib/types/src/host/integration/remote-job-tools.d.ts +0 -295
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +0 -97
- package/lib/types/src/host/integration/remote-tools.d.ts +0 -206
- package/lib/types/src/host/integration/root-registry.d.ts +0 -115
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/roots/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/roots/root-registry.d.ts +0 -144
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +0 -219
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +0 -52
- package/lib/types/src/host/integration/roots-api.d.ts +0 -407
- package/lib/types/src/host/integration/roots-wiring.d.ts +0 -211
- package/lib/types/src/host/integration/session/session-api.d.ts +0 -88
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session/session-header.d.ts +0 -151
- package/lib/types/src/host/integration/session/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-admin-service.d.ts +0 -236
- package/lib/types/src/host/integration/session-api.d.ts +0 -81
- package/lib/types/src/host/integration/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session-header.d.ts +0 -118
- package/lib/types/src/host/integration/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-scope.d.ts +0 -113
- package/lib/types/src/host/integration/session-tools.d.ts +0 -44
- package/lib/types/src/host/integration/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +0 -62
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +0 -60
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +0 -167
- package/lib/types/src/host/integration/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/tools/management-tools.d.ts +0 -245
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +0 -316
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +0 -43
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +0 -236
- package/lib/types/src/host/integration/tools/session-target.d.ts +0 -52
- package/lib/types/src/host/integration/tools/session-tools.d.ts +0 -46
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +0 -66
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +0 -48
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/transfer-adapters.d.ts +0 -44
- package/lib/types/src/host/integration/transfer-service.d.ts +0 -164
- package/lib/types/src/host/integration/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/user-roots.d.ts +0 -461
- package/lib/types/src/wiring/connections.d.ts +0 -189
- package/lib/types/src/wiring/fs.d.ts +0 -13
- package/lib/types/src/wiring/install-memo.d.ts +0 -32
- package/lib/types/src/wiring/legacy-dirs.d.ts +0 -62
- package/lib/types/src/wiring/package-paths.d.ts +0 -35
- package/lib/types/src/wiring/roots.d.ts +0 -21
- package/lib/types/src/wiring/state-dir.d.ts +0 -29
- package/lib/types/src/wiring/tools.d.ts +0 -35
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session-administration HTTP route: `/api/over-ssh/sessions`.
|
|
3
|
-
*
|
|
4
|
-
* The route is the client half's only way to reach the session store. It reuses the roots route's
|
|
5
|
-
* **same-origin trust check** ({@link trustVerdict}) rather than inventing a second policy: a request that may
|
|
6
|
-
* edit roots may also delete sessions, and one that may not, may not.
|
|
7
|
-
*
|
|
8
|
-
* ## Two rules that shape the wire
|
|
9
|
-
*
|
|
10
|
-
* 1. **`GET` never mutates.** It answers the inventory and the bin. Deletion and restore are `POST` actions,
|
|
11
|
-
* so a prefetch, a link preview or an over-eager browser cannot destroy anything.
|
|
12
|
-
* 2. **Irreversible actions are refused here unless the caller says it means it.** `purge` and
|
|
13
|
-
* `mode: 'permanent'` require `confirm: true` in the body. The UI already asks; this is the second lock, so
|
|
14
|
-
* a bug in the client cannot turn a recycle into an irreversible delete.
|
|
15
|
-
*
|
|
16
|
-
* @module dsh-over-ssh/integration/session-api
|
|
17
|
-
*/
|
|
18
|
-
import { type RootsApiRequest, type RootsApiResponse } from '../../app/roots/roots-api.ts';
|
|
19
|
-
import type { SessionAdminService } from '../../app/session-admin/session-admin-service.ts';
|
|
20
|
-
import type { SessionRecord, SessionTrashEntry } from '../../domain/session/session-admin.ts';
|
|
21
|
-
/** The one path this plugin claims for session administration. */
|
|
22
|
-
export declare const SESSION_API_PATH = "/api/over-ssh/sessions";
|
|
23
|
-
/** Largest accepted request body. An id list is a few hundred bytes. */
|
|
24
|
-
export declare const SESSION_API_MAX_BODY_BYTES: number;
|
|
25
|
-
/** One accepted `POST` action; anything else is refused with the list. */
|
|
26
|
-
export declare const SESSION_API_ACTIONS: readonly ["delete", "delete-workspace", "restore", "purge", "sweep", "preview"];
|
|
27
|
-
/** One accepted action. */
|
|
28
|
-
export type SessionApiAction = (typeof SESSION_API_ACTIONS)[number];
|
|
29
|
-
/** One inventory row as the wire carries it (a field-by-field projection, never the record itself). */
|
|
30
|
-
export interface SessionRow {
|
|
31
|
-
readonly id: string;
|
|
32
|
-
readonly classification: 'normal' | 'subsession' | 'unreadable';
|
|
33
|
-
readonly parentSessionId?: string;
|
|
34
|
-
readonly title?: string;
|
|
35
|
-
readonly cwd?: string;
|
|
36
|
-
/** Display title of the workspace owning `cwd`; absent when no registered workspace owns it. */
|
|
37
|
-
readonly workspaceTitle?: string;
|
|
38
|
-
readonly bytes: number;
|
|
39
|
-
readonly updatedAtMs: number;
|
|
40
|
-
/**
|
|
41
|
-
* The host holds this session in its store: it was opened in this DSH process.
|
|
42
|
-
*
|
|
43
|
-
* Not a running state — see {@link running}. The panel renders it as 已打开.
|
|
44
|
-
*/
|
|
45
|
-
readonly live: boolean;
|
|
46
|
-
/** The session's agent is running a turn right now (the platform's own "运行中" signal). */
|
|
47
|
-
readonly running: boolean;
|
|
48
|
-
}
|
|
49
|
-
/** One bin row as the wire carries it. */
|
|
50
|
-
export interface SessionBinRow {
|
|
51
|
-
readonly entryId: string;
|
|
52
|
-
readonly sessionId: string;
|
|
53
|
-
readonly title?: string;
|
|
54
|
-
readonly bytes: number;
|
|
55
|
-
readonly deletedAtMs: number;
|
|
56
|
-
readonly movedBy: 'rename' | 'copy';
|
|
57
|
-
readonly confirmedBy?: string;
|
|
58
|
-
}
|
|
59
|
-
/** The seams the route needs. */
|
|
60
|
-
export interface SessionApiDeps {
|
|
61
|
-
/** The session administration service (or a getter, since services can appear after activation). */
|
|
62
|
-
readonly service: () => SessionAdminService | undefined;
|
|
63
|
-
/** Retention limits used by `sweep`, from configuration. */
|
|
64
|
-
readonly policy: () => {
|
|
65
|
-
readonly maxEntries: number;
|
|
66
|
-
readonly maxBytes: number;
|
|
67
|
-
readonly maxAgeMs: number;
|
|
68
|
-
};
|
|
69
|
-
/** Injected clock, so a test can pin `sweep`'s "now". */
|
|
70
|
-
readonly now?: () => number;
|
|
71
|
-
}
|
|
72
|
-
/** The handler pair the entry registers. */
|
|
73
|
-
export interface SessionApi {
|
|
74
|
-
readonly handle: (req: RootsApiRequest, res: RootsApiResponse) => Promise<void>;
|
|
75
|
-
dispose(): void;
|
|
76
|
-
}
|
|
77
|
-
/** Project one inventory record to the wire shape. */
|
|
78
|
-
export declare function toSessionRow(record: SessionRecord): SessionRow;
|
|
79
|
-
/** Project one bin entry to the wire shape. */
|
|
80
|
-
export declare function toSessionBinRow(entry: SessionTrashEntry): SessionBinRow;
|
|
81
|
-
/**
|
|
82
|
-
* Build the handler pair.
|
|
83
|
-
*
|
|
84
|
-
* @param deps - injected seams.
|
|
85
|
-
* @returns `handle` plus a no-op `dispose`.
|
|
86
|
-
*/
|
|
87
|
-
export declare function createSessionApi(deps: SessionApiDeps): SessionApi;
|
|
88
|
-
//# sourceMappingURL=session-api.d.ts.map
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A cross-process advisory lock for the session recycle-bin index.
|
|
3
|
-
*
|
|
4
|
-
* Why this exists: the in-process promise chain in `session-admin-service.ts` cannot see **another DSH
|
|
5
|
-
* process**. Two processes deleting a session at the same time can each read the old index and then each
|
|
6
|
-
* write back their own version, so one entry is lost — and a lost entry is not a cosmetic problem: the
|
|
7
|
-
* moved bytes stay on disk while the index does not mention them, making them invisible *and* unrestorable.
|
|
8
|
-
*
|
|
9
|
-
* ## Design
|
|
10
|
-
*
|
|
11
|
-
* A lock file created with `wx` (exclusive create) is the mutual exclusion primitive: the filesystem
|
|
12
|
-
* guarantees exactly one creator wins, on local and network filesystems alike, without any dependency.
|
|
13
|
-
* The file records `{ pid, host, acquiredAtMs }` so a human can see who holds it.
|
|
14
|
-
*
|
|
15
|
-
* **Stale locks are reclaimed by age.** A process that dies while holding the lock leaves the file behind;
|
|
16
|
-
* without reclamation every later deletion would fail forever. The threshold is deliberately generous
|
|
17
|
-
* (see {@link DEFAULT_STALE_LOCK_MS}) because reclaiming a *live* holder's lock is worse than waiting: it
|
|
18
|
-
* reintroduces exactly the race this module prevents.
|
|
19
|
-
*
|
|
20
|
-
* This is advisory, not a guarantee: it coordinates cooperating processes of this plugin, and does not
|
|
21
|
-
* protect the bin from a process that ignores the protocol.
|
|
22
|
-
*
|
|
23
|
-
* @module dsh-over-ssh/integration/session-bin-lock
|
|
24
|
-
*/
|
|
25
|
-
/** How long a lock may sit before another process may reclaim it. */
|
|
26
|
-
export declare const DEFAULT_STALE_LOCK_MS = 60000;
|
|
27
|
-
/** How long to wait between acquisition attempts. */
|
|
28
|
-
export declare const DEFAULT_LOCK_RETRY_MS = 25;
|
|
29
|
-
/** How long to keep trying before giving up and reporting contention. */
|
|
30
|
-
export declare const DEFAULT_LOCK_TIMEOUT_MS = 30000;
|
|
31
|
-
/** Options for {@link createSessionBinLock}. */
|
|
32
|
-
export interface SessionBinLockOptions {
|
|
33
|
-
/** Lock file path. */
|
|
34
|
-
readonly path: string;
|
|
35
|
-
/** Stale threshold; non-positive disables reclamation (waiting forever is then the caller's risk). */
|
|
36
|
-
readonly staleMs?: number;
|
|
37
|
-
readonly retryMs?: number;
|
|
38
|
-
readonly timeoutMs?: number;
|
|
39
|
-
/** Injected sleep so tests need no real waiting. */
|
|
40
|
-
readonly sleep?: (ms: number) => Promise<void>;
|
|
41
|
-
/** Injected clock. */
|
|
42
|
-
readonly now?: () => number;
|
|
43
|
-
/** Identity recorded in the lock file (defaults to pid/host). */
|
|
44
|
-
readonly owner?: string;
|
|
45
|
-
/** Called when a stale lock is reclaimed, so the event is observable instead of silent. */
|
|
46
|
-
readonly onReclaim?: (event: {
|
|
47
|
-
readonly path: string;
|
|
48
|
-
readonly ageMs: number;
|
|
49
|
-
readonly holder: Partial<SessionBinLockRecord>;
|
|
50
|
-
}) => void;
|
|
51
|
-
}
|
|
52
|
-
/** What one lock file records. */
|
|
53
|
-
export interface SessionBinLockRecord {
|
|
54
|
-
readonly pid: number;
|
|
55
|
-
readonly host: string;
|
|
56
|
-
readonly acquiredAtMs: number;
|
|
57
|
-
/** Human-readable holder, for diagnosis (see `onReclaim`). **Not** used for ownership checks. */
|
|
58
|
-
readonly owner?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Per-acquisition ownership token, compared before release.
|
|
61
|
-
*
|
|
62
|
-
* Separate from `owner` on purpose: `owner` is documented as a **diagnostic** field and is asserted as
|
|
63
|
-
* such by `tests/session-bin-lock.test.ts`; overloading it with a token would change what a reader of
|
|
64
|
-
* the lock file sees. This field exists only so a holder whose lock was reclaimed cannot delete its
|
|
65
|
-
* successor's lock.
|
|
66
|
-
*/
|
|
67
|
-
readonly token?: string;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Run one operation under the cross-process lock.
|
|
71
|
-
*
|
|
72
|
-
* @param options - lock file path plus timing knobs.
|
|
73
|
-
* @returns a `withLock(key, operation)` function; `key` is accepted for interface compatibility and is not
|
|
74
|
-
* used to derive the path (there is exactly one session-bin index).
|
|
75
|
-
*/
|
|
76
|
-
export declare function createSessionBinLock(options: SessionBinLockOptions): {
|
|
77
|
-
withLock: <T>(_key: string, operation: () => Promise<T>) => Promise<T>;
|
|
78
|
-
lockPath: string;
|
|
79
|
-
};
|
|
80
|
-
//# sourceMappingURL=session-bin-lock.d.ts.map
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read a session log's header line, with a **bounded** memory footprint.
|
|
3
|
-
*
|
|
4
|
-
* Why this is its own module: the reader used to live in the composition root (`src/index.ts`), where no
|
|
5
|
-
* test can reach it (审计 0046 记录了同款教训). A file-system reader is exactly the kind of code that needs a
|
|
6
|
-
* test to pin its *bounds*, and the bound is the whole point here.
|
|
7
|
-
*
|
|
8
|
-
* ## What went wrong before
|
|
9
|
-
*
|
|
10
|
-
* The old reader claimed to "decompress only the head" but read the **whole file** first:
|
|
11
|
-
*
|
|
12
|
-
* ```ts
|
|
13
|
-
* const handle = await readFile(path); // 10.9 MiB for one real session
|
|
14
|
-
* text = zstdDecompressSync(handle.subarray(0, 256 * 1024)).toString(); // cap applies to the SLICE only
|
|
15
|
-
* } catch {
|
|
16
|
-
* text = zstdDecompressSync(await readFile(path)).toString(); // and the fallback reads it AGAIN
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*
|
|
20
|
-
* `readFile` has no `maxBytes`, so the 256 KiB constant bounded the decompressor's *input*, never the read.
|
|
21
|
-
* Measured on the reference machine's real store (73 sessions): **105.3 MiB** read into memory to extract
|
|
22
|
-
* 73 first lines (19 KiB of output), worst single session **10.9 MiB**, and 51 of 73 logs exceeded the
|
|
23
|
-
* "cap" that was supposedly in force. See `doc/06-audits/0050`.
|
|
24
|
-
*
|
|
25
|
-
* ## What went wrong the second time (2026-10-31)
|
|
26
|
-
*
|
|
27
|
-
* The fallback decompressed the head with `zstdDecompressSync`, which requires a **complete frame** and answers
|
|
28
|
-
* `Z_BUF_ERROR` for anything shorter. That was true of the store it was written against (one small frame per
|
|
29
|
-
* record) and is **not** true of the current one: a 13.2 MB session compresses its head into a frame far larger
|
|
30
|
-
* than any bounded read, so no cap could ever be "complete". Measured on this machine's real store: 8 of 19
|
|
31
|
-
* sessions in one workspace were classified `unreadable`, and every one had an intact header line — 64 KiB of a
|
|
32
|
-
* 13.2 MB log stream-decodes to 184 KB of text whose first line is the header. Users saw it as
|
|
33
|
-
* 「该会话无法读取,不能删除」 on a perfectly healthy session.
|
|
34
|
-
*
|
|
35
|
-
* The fix is to decode **incrementally**: feed the bounded prefix to a stream decoder, keep the output until the
|
|
36
|
-
* first newline, then stop. A truncated frame ending in an `error` event is the normal case here.
|
|
37
|
-
*
|
|
38
|
-
* ## The contract this module implements
|
|
39
|
-
*
|
|
40
|
-
* 1. **Never read more than `cap` bytes of any one file.** The read is a positioned `read(2)` off a
|
|
41
|
-
* descriptor, so the file's size is irrelevant to peak memory.
|
|
42
|
-
* 2. **The fallback is bounded too.** A slice that ends inside a zstd frame can be rejected by the
|
|
43
|
-
* decoder; the retry asks for a larger head (`retryBytes`), never for the whole file.
|
|
44
|
-
* 3. **Failure is `undefined`, never a throw.** A directory with no log, a corrupt frame, or an
|
|
45
|
-
* unparsable first line all yield `undefined`, so the inventory classifies the session as
|
|
46
|
-
* `unreadable` instead of guessing from the lossy directory name (ADR-0009).
|
|
47
|
-
*
|
|
48
|
-
* @module dsh-over-ssh/integration/session-header
|
|
49
|
-
*/
|
|
50
|
-
import type { SessionHeader } from '../../app/session-admin/session-admin-service.ts';
|
|
51
|
-
/**
|
|
52
|
-
* Bytes read from the head of a session log on the first attempt.
|
|
53
|
-
*
|
|
54
|
-
* Chosen from measurement, not taste. On the reference machine's real store (73 sessions, `0050`):
|
|
55
|
-
*
|
|
56
|
-
* - the largest **header line** is **309 B** of UTF-8 (p50 279 B);
|
|
57
|
-
* - the largest compressed frame carrying a lone header is **253 B**;
|
|
58
|
-
* - the smallest head that recovered **all 73** headers is **4 KiB**.
|
|
59
|
-
*
|
|
60
|
-
* 64 KiB is therefore **16x** the empirically sufficient head and **212x** the largest header line, while
|
|
61
|
-
* being 4x smaller than the previous (and, as it turned out, entirely non-binding) 256 KiB constant.
|
|
62
|
-
*/
|
|
63
|
-
export declare const SESSION_HEADER_READ_BYTES: number;
|
|
64
|
-
/**
|
|
65
|
-
* Bytes read on the retry, when the first slice ended inside the frame.
|
|
66
|
-
*
|
|
67
|
-
* Still bounded, and still independent of the file's real size — that is the property the old fallback
|
|
68
|
-
* broke. A log whose header genuinely cannot be recovered from 1 MiB is reported `unreadable`.
|
|
69
|
-
*/
|
|
70
|
-
export declare const SESSION_HEADER_RETRY_BYTES: number;
|
|
71
|
-
/** The I/O this reader needs; injected so a test can pin the byte bounds exactly (AGENTS.md 红线 3). */
|
|
72
|
-
export interface SessionHeaderIo {
|
|
73
|
-
/** List one directory's entry names. */
|
|
74
|
-
readonly listDir: (dir: string) => Promise<readonly string[]>;
|
|
75
|
-
/**
|
|
76
|
-
* Read at most `maxBytes` from the **head** of one file.
|
|
77
|
-
*
|
|
78
|
-
* An implementation must not read the whole file and slice: that is precisely the defect this module
|
|
79
|
-
* exists to prevent. It may return fewer bytes (a short file, or a short read).
|
|
80
|
-
*/
|
|
81
|
-
readonly readHead: (path: string, maxBytes: number) => Promise<Uint8Array>;
|
|
82
|
-
/**
|
|
83
|
-
* Decompress a zstd frame **or frame prefix** to UTF-8 text.
|
|
84
|
-
*
|
|
85
|
-
* May be async: a prefix has to be decoded incrementally (see {@link decompressPrefix}), and a partial frame
|
|
86
|
-
* is expected rather than exceptional.
|
|
87
|
-
*/
|
|
88
|
-
readonly decompress: (bytes: Uint8Array) => string | Promise<string>;
|
|
89
|
-
/** Head caps to try, in order. Defaults to `[SESSION_HEADER_READ_BYTES, SESSION_HEADER_RETRY_BYTES]`. */
|
|
90
|
-
readonly caps?: readonly number[];
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Parse the first JSONL line into the header fields this feature needs.
|
|
94
|
-
*
|
|
95
|
-
* @param text - decompressed log text (only the head is present).
|
|
96
|
-
* @returns the header, or `undefined` when the first line is absent or not a session record.
|
|
97
|
-
*/
|
|
98
|
-
export declare function parseSessionHeaderLine(text: string): SessionHeader | undefined;
|
|
99
|
-
/**
|
|
100
|
-
* Build the header reader.
|
|
101
|
-
*
|
|
102
|
-
* Every failure is reported as `undefined` rather than thrown: an inventory must survive one unreadable
|
|
103
|
-
* session.
|
|
104
|
-
*
|
|
105
|
-
* @param io - directory listing, bounded head read and decompressor.
|
|
106
|
-
* @returns a reader from one session directory to its header, or `undefined` when unreadable.
|
|
107
|
-
*/
|
|
108
|
-
export declare function createSessionHeaderReader(io: SessionHeaderIo): (dir: string) => Promise<SessionHeader | undefined>;
|
|
109
|
-
/**
|
|
110
|
-
* Decode a zstd frame **prefix** to text, stopping at the first complete line.
|
|
111
|
-
*
|
|
112
|
-
* Why not `zstdDecompressSync`: it requires a complete frame and answers `Z_BUF_ERROR` for a prefix. The current
|
|
113
|
-
* session store writes frames far larger than any bounded read, so every such log was classified `unreadable`
|
|
114
|
-
* (see the module note). The stream decoder emits what it can from the prefix it was given; a truncated frame
|
|
115
|
-
* ends in an `error` event with the partial output already delivered, which is the **normal** case here.
|
|
116
|
-
*
|
|
117
|
-
* Bounded on purpose: collection stops at the first newline (the header is one line) and the stream is
|
|
118
|
-
* destroyed, so decoding a 13 MB log's head never materialises the body.
|
|
119
|
-
*
|
|
120
|
-
* @param bytes - the bounded head read from the log.
|
|
121
|
-
* @returns the decoded text (possibly a prefix, possibly empty), never a throw.
|
|
122
|
-
*/
|
|
123
|
-
export declare function decompressPrefix(bytes: Uint8Array): Promise<string>;
|
|
124
|
-
/**
|
|
125
|
-
* Read at most `maxBytes` from the head of a file, off a descriptor.
|
|
126
|
-
*
|
|
127
|
-
* A positioned read is what makes the bound real: `readFile` would materialise the entire log first.
|
|
128
|
-
*
|
|
129
|
-
* @param path - file to read.
|
|
130
|
-
* @param maxBytes - hard ceiling on the bytes returned.
|
|
131
|
-
* @returns the bytes actually read (possibly fewer, possibly none).
|
|
132
|
-
*/
|
|
133
|
-
export declare function readHeadFromDisk(path: string, maxBytes: number): Promise<Uint8Array>;
|
|
134
|
-
/**
|
|
135
|
-
* The production I/O: real `readdir`, a **bounded** head read and the real zstd decoder.
|
|
136
|
-
*
|
|
137
|
-
* Exported so a test can pin the one property the reader's whole contract rests on — that the production
|
|
138
|
-
* `readHead` respects its cap. Exercising the assembled reader through an *injected* fake cannot catch a
|
|
139
|
-
* regression in this wiring, because the fake stays bounded either way; a mutation experiment during
|
|
140
|
-
* audit 0050 exposed exactly that gap.
|
|
141
|
-
*
|
|
142
|
-
* @returns the production I/O bundle.
|
|
143
|
-
*/
|
|
144
|
-
export declare function createDiskSessionHeaderIo(): SessionHeaderIo;
|
|
145
|
-
/**
|
|
146
|
-
* The production reader: real `readdir`, a bounded head read and the real zstd decoder.
|
|
147
|
-
*
|
|
148
|
-
* @returns a reader from one session directory to its header, or `undefined` when unreadable.
|
|
149
|
-
*/
|
|
150
|
-
export declare function createDiskSessionHeaderReader(): (dir: string) => Promise<SessionHeader | undefined>;
|
|
151
|
-
//# sourceMappingURL=session-header.d.ts.map
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the two human labels a session listing needs: the session's own title and the display name of the
|
|
3
|
-
* workspace that owns its directory.
|
|
4
|
-
*
|
|
5
|
-
* Both live in **optional platform services**, and both are read through a structural context shape rather
|
|
6
|
-
* than a Cordis import, so this module is unit-testable with a plain object and the domain layer keeps its
|
|
7
|
-
* no-`@deepseek-ai/*` rule.
|
|
8
|
-
*
|
|
9
|
-
* Why this is its own module: these are the only two places where the session feature reaches into the
|
|
10
|
-
* platform, and the composition root that calls them cannot be exercised by a test. Anything decision-like
|
|
11
|
-
* that lands there is untested by construction (审计 0046 记录了同款教训), so the decisions live here.
|
|
12
|
-
*
|
|
13
|
-
* @module dsh-over-ssh/integration/session-labels
|
|
14
|
-
*/
|
|
15
|
-
import type { SessionHeader } from '../../app/session-admin/session-admin-service.ts';
|
|
16
|
-
/** The one context method these resolvers need, kept structural so no Cordis import is required. */
|
|
17
|
-
export interface OptionalServiceLookup {
|
|
18
|
-
get(name: string): unknown;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a session's generated title through the platform's projection cache.
|
|
22
|
-
*
|
|
23
|
-
* The title is a **projection**, never header metadata: the header's own `title` field is always absent (all
|
|
24
|
-
* 88 sessions on the reference machine had none), which is why the panel could only ever show a uuid. The
|
|
25
|
-
* cache is the sanctioned zero-I/O listing read — `cachedSnapshot` validates the stored row against the
|
|
26
|
-
* header's lifecycle identity (`formatVersion`, `createdAt`, `cwd`, `isSeeded`), so a title from a different
|
|
27
|
-
* lifecycle that reused the same id can never be served.
|
|
28
|
-
*
|
|
29
|
-
* Every failure is benign and yields `undefined`: a session persisted before the cache was composed has no
|
|
30
|
-
* row until it is opened once, and the row's unit version must match the live title projection. The caller
|
|
31
|
-
* then falls back to the workspace, and finally to the id.
|
|
32
|
-
*
|
|
33
|
-
* The service is looked up **per call**, not once at activation: on this runtime an optional service can
|
|
34
|
-
* still be absent while a plugin entry activates (`session-route-wiring.ts` documents the same trap for
|
|
35
|
-
* `webServer`), and a one-time check would then disable titles for the whole process lifetime.
|
|
36
|
-
*
|
|
37
|
-
* @param ctx - anything with cordis's `get`; usually the plugin context.
|
|
38
|
-
* @returns a resolver from header to title; it yields `undefined` whenever the service or the row is absent.
|
|
39
|
-
*/
|
|
40
|
-
export declare function createSessionTitleResolver(ctx: OptionalServiceLookup): (header: SessionHeader) => string | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a `cwd` to its registered workspace's display title.
|
|
43
|
-
*
|
|
44
|
-
* The registry is the platform's own durable mapping from directory to a human name (`libaw_vep`,
|
|
45
|
-
* `a733_android13`), which is exactly the label a session list needs and the one the sidebar shows. A
|
|
46
|
-
* directory no workspace owns yields `undefined`; the client then uses the path's leaf name, so a session
|
|
47
|
-
* outside a workspace is still identifiable.
|
|
48
|
-
*
|
|
49
|
-
* Like {@link createSessionTitleResolver}, the service is looked up **per call** so a late-arriving registry
|
|
50
|
-
* still works.
|
|
51
|
-
*
|
|
52
|
-
* @param ctx - anything with cordis's `get`; usually the plugin context.
|
|
53
|
-
* @returns a resolver from `cwd` to workspace title; it yields `undefined` whenever none applies.
|
|
54
|
-
*/
|
|
55
|
-
export declare function createWorkspaceTitleResolver(ctx: OptionalServiceLookup): (cwd: string | undefined) => string | undefined;
|
|
56
|
-
//# sourceMappingURL=session-labels.d.ts.map
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Own the session-administration route (`/api/over-ssh/sessions`) on the browser carrier.
|
|
3
|
-
*
|
|
4
|
-
* This is the same **deferred carrier** dance as `roots-wiring.ts`, and for the same measured reason: on the
|
|
5
|
-
* packaged 0.2.0-rc.2 runtime `ctx.get('webServer')` is `undefined` while a plugin entry activates (the
|
|
6
|
-
* carrier row is constructed later), so a `ctx.get` lookup silently skips registration and every request
|
|
7
|
-
* answers `http-404` from the SPA fallback while the plugin looks perfectly healthy.
|
|
8
|
-
* `ctx.inject(['webServer'], ...)` resolves the service when it appears.
|
|
9
|
-
*
|
|
10
|
-
* A profile without a carrier (headless) is **not** an error: the plugin works, the panel simply has no
|
|
11
|
-
* session data channel. The reason is collected as a warning the entry logs once `ctx.logger` resolves.
|
|
12
|
-
*
|
|
13
|
-
* @module dsh-over-ssh/integration/session-route-wiring
|
|
14
|
-
*/
|
|
15
|
-
import { type SessionApi, type SessionApiDeps } from './session-api.ts';
|
|
16
|
-
/** The browser HTTP carrier, narrowed to the one method this wiring calls. */
|
|
17
|
-
export interface SessionRouteHost {
|
|
18
|
-
register(route: {
|
|
19
|
-
readonly kind: 'exact';
|
|
20
|
-
readonly path: string;
|
|
21
|
-
readonly handler: (req: never, res: never) => Promise<void>;
|
|
22
|
-
}): () => void;
|
|
23
|
-
}
|
|
24
|
-
/** The provider that hands out the carrier once it exists. */
|
|
25
|
-
export interface SessionRouteHostProvider {
|
|
26
|
-
inject(names: readonly string[], callback: (scoped: {
|
|
27
|
-
get(name: string): unknown;
|
|
28
|
-
}) => void | (() => void)): unknown;
|
|
29
|
-
}
|
|
30
|
-
/** Everything the wiring needs from the entry. */
|
|
31
|
-
export interface SessionRouteWiringDeps extends SessionApiDeps {
|
|
32
|
-
readonly webServerProvider?: SessionRouteHostProvider | undefined;
|
|
33
|
-
}
|
|
34
|
-
/** What the entry holds on to. */
|
|
35
|
-
export interface SessionRouteWiring {
|
|
36
|
-
readonly api: SessionApi;
|
|
37
|
-
/** Human-readable reasons the route may not be live; logged after activation. */
|
|
38
|
-
readonly routeWarnings: () => readonly string[];
|
|
39
|
-
/** Whether the deferred carrier callback ever ran. */
|
|
40
|
-
readonly injectRan: () => boolean;
|
|
41
|
-
readonly dispose: () => void;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Register the route as soon as the carrier exists, and again if it ever comes back.
|
|
45
|
-
*
|
|
46
|
-
* @param deps - the API seams plus the carrier provider.
|
|
47
|
-
* @returns the wiring handle.
|
|
48
|
-
*/
|
|
49
|
-
export declare function createSessionRouteWiring(deps: SessionRouteWiringDeps): SessionRouteWiring;
|
|
50
|
-
//# sourceMappingURL=session-route-wiring.d.ts.map
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session administration service: inventory, deletion into the session bin, restore, purge, retention.
|
|
3
|
-
*
|
|
4
|
-
* The domain decisions live in [`domain/session-admin.ts`](../domain/session-admin.ts); this module wires them to
|
|
5
|
-
* injected I/O. Every filesystem and platform call arrives as a function (AGENTS.md 红线 3), so this module is
|
|
6
|
-
* testable offline and a test can make a move fail.
|
|
7
|
-
*
|
|
8
|
-
* ## Three contracts matter more than the rest
|
|
9
|
-
*
|
|
10
|
-
* 1. **Move first, confirm, then record.** `deleteSession` reports success only after the session directory is
|
|
11
|
-
* observed inside the bin **and** gone from the store. A move that throws is confirmed before anything is
|
|
12
|
-
* claimed: if it actually landed (a lost response), that counts as success; otherwise the call fails and
|
|
13
|
-
* **nothing** is recorded. "Deleted but not in the bin" must never be reported as success.
|
|
14
|
-
* 2. **The index write is never swallowed.** If the index cannot be written after a successful move, the move is
|
|
15
|
-
* rolled back and the failure is reported. A physical purge has no rollback, so that case is reported with
|
|
16
|
-
* `purged: true, indexStale: true` instead of being hidden.
|
|
17
|
-
* 3. **Live sessions are never touched.** The decision layer refuses them; this layer re-checks before moving, so
|
|
18
|
-
* a session that went live between preview and confirm is refused rather than killed.
|
|
19
|
-
*
|
|
20
|
-
* ## Where the bin lives (and why not under `sessions/`)
|
|
21
|
-
*
|
|
22
|
-
* The platform enumerates **every** directory under the sessions root (`listProjectDirs` has no dot-directory
|
|
23
|
-
* filter) and fails the whole listing when one session id appears twice. A bin inside `sessions/` that kept the
|
|
24
|
-
* original log filename would therefore make deleted sessions reappear *and* break the session list. The bin
|
|
25
|
-
* lives in the plugin's own storage instead — see ADR-0009.
|
|
26
|
-
*
|
|
27
|
-
* @module dsh-remote-workspace/integration/session-admin-service
|
|
28
|
-
*/
|
|
29
|
-
import { type DeleteDecision, type SessionRecord, type SessionTrashEntry, type SessionTrashEntryId } from '../domain/session-admin.ts';
|
|
30
|
-
/** One directory entry as the inventory sees it. */
|
|
31
|
-
export interface SessionDirEntry {
|
|
32
|
-
readonly name: string;
|
|
33
|
-
readonly isDirectory: boolean;
|
|
34
|
-
readonly bytes: number;
|
|
35
|
-
readonly mtimeMs: number;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Everything the service needs from the outside world.
|
|
39
|
-
*
|
|
40
|
-
* `readHeader` is deliberately the **only** way to learn about a session: the directory name cannot be decoded
|
|
41
|
-
* (see ADR-0009), so classification must come from the header.
|
|
42
|
-
*/
|
|
43
|
-
export interface SessionAdminDeps {
|
|
44
|
-
/** The platform's sessions root (e.g. `~/.dsh/sessions`). */
|
|
45
|
-
readonly sessionsRoot: string;
|
|
46
|
-
/** The plugin's own bin root; must **not** be inside {@link sessionsRoot}. */
|
|
47
|
-
readonly binRoot: string;
|
|
48
|
-
/** List one directory's entries; a missing directory yields an empty list, not a failure. */
|
|
49
|
-
readonly listDir: (path: string) => Promise<readonly SessionDirEntry[]>;
|
|
50
|
-
/** Read a session header. `undefined` means the log is unreadable (corrupt/unsupported). */
|
|
51
|
-
readonly readHeader: (sessionDir: string) => Promise<SessionHeader | undefined>;
|
|
52
|
-
/**
|
|
53
|
-
* Display title of the workspace that owns a session directory, or `undefined` when none does.
|
|
54
|
-
*
|
|
55
|
-
* Injected because the mapping lives in the platform's workspace registry, which the domain layer must not
|
|
56
|
-
* import (see the repository's red lines). A listing without this resolver still answers: every row keeps
|
|
57
|
-
* its `cwd`, so a reader falls back to the directory's own leaf name.
|
|
58
|
-
*/
|
|
59
|
-
readonly resolveWorkspaceTitle?: ((cwd: string | undefined) => string | undefined) | undefined;
|
|
60
|
-
/**
|
|
61
|
-
* The session's generated title, or `undefined` when it has none.
|
|
62
|
-
*
|
|
63
|
-
* The header carries no title (it is a projection, not header metadata), so this is the only way a listing
|
|
64
|
-
* can label a session by name. Injected for the same reason as {@link resolveWorkspaceTitle}; without it
|
|
65
|
-
* every row falls back to its id.
|
|
66
|
-
*/
|
|
67
|
-
readonly resolveSessionTitle?: ((header: SessionHeader) => string | undefined) | undefined;
|
|
68
|
-
/** `stat`; `undefined` when the path does not exist. */
|
|
69
|
-
readonly stat: (path: string) => Promise<{
|
|
70
|
-
readonly isDirectory: boolean;
|
|
71
|
-
readonly bytes: number;
|
|
72
|
-
} | undefined>;
|
|
73
|
-
/** Read a text file. */
|
|
74
|
-
readonly readText: (path: string) => Promise<string>;
|
|
75
|
-
/** Write a text file (UTF-8, no BOM). */
|
|
76
|
-
readonly writeText: (path: string, text: string) => Promise<void>;
|
|
77
|
-
/**
|
|
78
|
-
* Replace a text file **atomically** (write a temp file, then rename over the target).
|
|
79
|
-
*
|
|
80
|
-
* Optional, and worth supplying: without it a process killed mid-write leaves a truncated JSON index,
|
|
81
|
-
* which the parser then rejects as a hard failure. Atomic replace makes the index either the old or the
|
|
82
|
-
* new document, never a fragment.
|
|
83
|
-
*/
|
|
84
|
-
readonly writeTextAtomic?: (path: string, text: string) => Promise<void>;
|
|
85
|
-
/**
|
|
86
|
-
* Run `operation` while holding a **cross-process** lock for `key`.
|
|
87
|
-
*
|
|
88
|
-
* The in-process chain cannot see another DSH process; without this, two processes deleting at the same
|
|
89
|
-
* time can each read the old index and overwrite the other's entry — leaving bytes in the bin that no
|
|
90
|
-
* index mentions (invisible and unrestorable).
|
|
91
|
-
*
|
|
92
|
-
* A caller-supplied implementation must be **exclusive** and must not deadlock when the previous holder
|
|
93
|
-
* died (see `createSessionBinLock`, which reclaims a stale lock by age).
|
|
94
|
-
*/
|
|
95
|
-
readonly withLock?: <T>(key: string, operation: () => Promise<T>) => Promise<T>;
|
|
96
|
-
/**
|
|
97
|
-
* Move a directory. Must be atomic on one filesystem; the caller supplies the cross-device fallback.
|
|
98
|
-
*
|
|
99
|
-
* Returning `'copy'` means the implementation copied (cross-device) and the source is already gone.
|
|
100
|
-
*/
|
|
101
|
-
readonly moveDir: (from: string, to: string) => Promise<'rename' | 'copy'>;
|
|
102
|
-
/** Recursively remove a directory (used for purge and for rolling back a failed record). */
|
|
103
|
-
readonly removeDir: (path: string) => Promise<void>;
|
|
104
|
-
/** Copy a directory recursively (the cross-device fallback, when the deps implement it themselves). */
|
|
105
|
-
readonly copyDir?: (from: string, to: string) => Promise<void>;
|
|
106
|
-
/** Whether a session is currently held by a live agent; never deleted. */
|
|
107
|
-
readonly isLive: (sessionId: string) => boolean;
|
|
108
|
-
/** Remove a session from workspace bookkeeping (`detachSession`); optional where unavailable. */
|
|
109
|
-
readonly detachSession?: (sessionId: string) => Promise<void>;
|
|
110
|
-
/** Announce the removal so clients drop the row (`api-session/removed`). */
|
|
111
|
-
readonly emitRemoved?: (sessionId: string) => void;
|
|
112
|
-
/** Injected clock. */
|
|
113
|
-
readonly now: () => number;
|
|
114
|
-
/** Who confirmed, for the audit trail (FR-3.10); absent when the caller did not say. */
|
|
115
|
-
readonly confirmedBy?: string;
|
|
116
|
-
}
|
|
117
|
-
/** The parts of a session header this feature needs. */
|
|
118
|
-
export interface SessionHeader {
|
|
119
|
-
readonly id: string;
|
|
120
|
-
/** `cwd` from the header; `undefined` for the platform's no-cwd bucket. */
|
|
121
|
-
readonly cwd: string | undefined;
|
|
122
|
-
readonly title: string | undefined;
|
|
123
|
-
readonly parentSessionId: string | undefined;
|
|
124
|
-
/**
|
|
125
|
-
* Session format generation, as stored in the log's first line.
|
|
126
|
-
*
|
|
127
|
-
* Together with {@link createdAt} and {@link isSeeded} this is the **lifecycle identity** a header alone
|
|
128
|
-
* witnesses, and it is what the platform's projection cache validates a stored title row against. A
|
|
129
|
-
* resolver that skipped it could serve a title from a different lifecycle that reused the same id.
|
|
130
|
-
*/
|
|
131
|
-
readonly version?: number | undefined;
|
|
132
|
-
readonly createdAt?: number | undefined;
|
|
133
|
-
readonly isSeeded?: boolean | undefined;
|
|
134
|
-
}
|
|
135
|
-
/** Options shared by the destructive calls. */
|
|
136
|
-
export interface SessionAdminOptions {
|
|
137
|
-
readonly signal?: AbortSignal;
|
|
138
|
-
}
|
|
139
|
-
/** Result of a session deletion. */
|
|
140
|
-
export interface DeleteSessionResult {
|
|
141
|
-
readonly mode: 'recycle' | 'permanent';
|
|
142
|
-
/** The bin entry in `recycle` mode; `undefined` for `permanent`. */
|
|
143
|
-
readonly entry: SessionTrashEntry | undefined;
|
|
144
|
-
/** Whether the content can still be recovered. **Always `false` for `permanent`.** */
|
|
145
|
-
readonly recoverable: boolean;
|
|
146
|
-
/** True when the move was observed to have landed (store gone + bin present). */
|
|
147
|
-
readonly verified: boolean;
|
|
148
|
-
/** True when the bytes crossed a filesystem boundary and were copied. */
|
|
149
|
-
readonly copied: boolean;
|
|
150
|
-
/** Ids that were refused, with the reason (a partial deletion is reported, never hidden). */
|
|
151
|
-
readonly refusals: DeleteDecision['refusals'];
|
|
152
|
-
readonly bytes: number;
|
|
153
|
-
}
|
|
154
|
-
/** Result of a restore. */
|
|
155
|
-
export interface RestoreSessionResult {
|
|
156
|
-
readonly entry: SessionTrashEntry;
|
|
157
|
-
/** Absolute path the session directory was moved back to. */
|
|
158
|
-
readonly restoredPath: string;
|
|
159
|
-
readonly movedBy: 'rename' | 'copy';
|
|
160
|
-
}
|
|
161
|
-
/** One purge outcome. */
|
|
162
|
-
export interface PurgeSessionResult {
|
|
163
|
-
readonly id: SessionTrashEntryId;
|
|
164
|
-
readonly sessionId: string;
|
|
165
|
-
readonly purged: boolean;
|
|
166
|
-
/** True when the entry was removed physically but the index could not be updated afterwards. */
|
|
167
|
-
readonly indexStale: boolean;
|
|
168
|
-
}
|
|
169
|
-
/** A retention sweep outcome for the session bin. */
|
|
170
|
-
export interface SweepSessionResult {
|
|
171
|
-
readonly purged: readonly PurgeSessionResult[];
|
|
172
|
-
readonly failed: readonly {
|
|
173
|
-
readonly id: SessionTrashEntryId;
|
|
174
|
-
readonly reason: string;
|
|
175
|
-
}[];
|
|
176
|
-
readonly remainingEntries: number;
|
|
177
|
-
readonly remainingBytes: number;
|
|
178
|
-
}
|
|
179
|
-
/** The session-bin retention policy (same three dimensions as the file-level bin). */
|
|
180
|
-
export interface SessionBinPolicy {
|
|
181
|
-
readonly maxEntries: number;
|
|
182
|
-
readonly maxBytes: number;
|
|
183
|
-
readonly maxAgeMs: number;
|
|
184
|
-
}
|
|
185
|
-
/** The service surface. */
|
|
186
|
-
export interface SessionAdminService {
|
|
187
|
-
/** Inventory every session in the store (FR-3.3). */
|
|
188
|
-
inventory(options?: SessionAdminOptions): Promise<readonly SessionRecord[]>;
|
|
189
|
-
/** Preview a deletion without touching anything (FR-3.7). */
|
|
190
|
-
previewDelete(ids: readonly string[], options?: SessionAdminOptions & {
|
|
191
|
-
readonly keep?: readonly string[];
|
|
192
|
-
}): Promise<DeleteDecision>;
|
|
193
|
-
/** Delete sessions: into the bin (`recycle`) or irreversibly (`permanent`). */
|
|
194
|
-
deleteSession(ids: readonly string[], options?: SessionAdminOptions & {
|
|
195
|
-
readonly mode?: 'recycle' | 'permanent';
|
|
196
|
-
readonly keep?: readonly string[];
|
|
197
|
-
}): Promise<DeleteSessionResult>;
|
|
198
|
-
/**
|
|
199
|
-
* Delete every session that belongs to one workspace directory, and nothing else.
|
|
200
|
-
*
|
|
201
|
-
* The workspace itself is **left registered**: the user asked to clear the workspace's sessions, not to
|
|
202
|
-
* unregister the workspace (and the platform's `workspaceRegistry.delete` would also be a different,
|
|
203
|
-
* separately-confirmed action). Sessions are matched by their header's `cwd` against the given directory,
|
|
204
|
-
* folded the same way the label resolver folds paths, so a Windows case/separator difference still matches.
|
|
205
|
-
*
|
|
206
|
-
* Live sessions are refused by the same rules as {@link deleteSession} (they appear in `refusals`), so a
|
|
207
|
-
* workspace containing a running session is deleted **except** for that session rather than failing whole.
|
|
208
|
-
*
|
|
209
|
-
* @param cwd - the workspace directory whose sessions should go.
|
|
210
|
-
* @param options - deletion mode plus cancellation.
|
|
211
|
-
* @returns the ids deleted, the per-id refusals, and the bytes freed.
|
|
212
|
-
*/
|
|
213
|
-
deleteWorkspaceSessions(cwd: string, options?: SessionAdminOptions & {
|
|
214
|
-
readonly mode?: 'recycle' | 'permanent';
|
|
215
|
-
}): Promise<DeleteSessionResult & {
|
|
216
|
-
readonly matched: number;
|
|
217
|
-
}>;
|
|
218
|
-
/** List the bin. */
|
|
219
|
-
list(options?: SessionAdminOptions): Promise<readonly SessionTrashEntry[]>;
|
|
220
|
-
/** Move one entry back to where it came from. */
|
|
221
|
-
restore(entryId: string, options?: SessionAdminOptions): Promise<RestoreSessionResult>;
|
|
222
|
-
/** Delete one entry physically (the irreversible step, after the retention period). */
|
|
223
|
-
purge(entryId: string, options?: SessionAdminOptions): Promise<PurgeSessionResult>;
|
|
224
|
-
/** Apply the retention policy. */
|
|
225
|
-
sweep(policy: SessionBinPolicy, options?: SessionAdminOptions): Promise<SweepSessionResult>;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Build the session-administration service.
|
|
229
|
-
*
|
|
230
|
-
* @param deps - injected I/O.
|
|
231
|
-
* @returns the service.
|
|
232
|
-
* @throws SshDevError `ROOT_INVALID` when the bin root is inside the sessions root (that placement breaks the
|
|
233
|
-
* platform's session listing — see the module note and ADR-0009).
|
|
234
|
-
*/
|
|
235
|
-
export declare function createSessionAdminService(deps: SessionAdminDeps): SessionAdminService;
|
|
236
|
-
//# sourceMappingURL=session-admin-service.d.ts.map
|