@struktoai/mirage-dsh 0.0.7-alpha.2 → 0.0.7
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/LICENSE +201 -0
- package/README.md +28 -29
- package/cordis.patch.yml +38 -6
- package/dist/{chunk-M4JOF35U.js → chunk-FRF6WPLN.js} +133 -44
- package/dist/chunk-FRF6WPLN.js.map +1 -0
- package/dist/chunk-G2GHA2F6.js +211 -0
- package/dist/chunk-G2GHA2F6.js.map +1 -0
- package/dist/{chunk-QTBMLMKT.js → chunk-KUIV2Z3M.js} +243 -326
- package/dist/chunk-KUIV2Z3M.js.map +1 -0
- package/dist/{chunk-4CZV24HW.js → chunk-OHZACFWO.js} +36 -14
- package/dist/chunk-OHZACFWO.js.map +1 -0
- package/dist/{chunk-2F2PJ7D7.js → chunk-OZ2F4R6T.js} +74 -17
- package/dist/chunk-OZ2F4R6T.js.map +1 -0
- package/dist/chunk-QH5H3VND.js +110 -0
- package/dist/chunk-QH5H3VND.js.map +1 -0
- package/dist/chunk-T4BSJAGN.js +64 -0
- package/dist/chunk-T4BSJAGN.js.map +1 -0
- package/dist/{fs-B3Zi9oo6.d.ts → fs-BNOIFi9o.d.ts} +48 -4
- package/dist/index.d.ts +8 -3
- package/dist/index.js +15 -6
- package/dist/plugin/fs.d.ts +1 -1
- package/dist/plugin/fs.js +2 -2
- package/dist/plugin/service.d.ts +2 -1
- package/dist/plugin/service.js +1 -1
- package/dist/plugin/shell.d.ts +1 -1
- package/dist/plugin/shell.js +3 -2
- package/dist/plugin/spill-store.d.ts +3 -0
- package/dist/plugin/spill-store.js +8 -0
- package/dist/plugin/spill-store.js.map +1 -0
- package/dist/plugin/subprocess.d.ts +8 -0
- package/dist/plugin/subprocess.js +9 -0
- package/dist/plugin/subprocess.js.map +1 -0
- package/dist/{service-BK6NR_RX.d.ts → service-vCjLPYMI.d.ts} +29 -17
- package/dist/{shell-BTf2wsDy.d.ts → shell-BAyBocjm.d.ts} +25 -35
- package/dist/spill-store-BfljgRi0.d.ts +38 -0
- package/dist/subprocess-CInesMpR.d.ts +23 -0
- package/package.json +24 -12
- package/dist/chunk-2F2PJ7D7.js.map +0 -1
- package/dist/chunk-4CZV24HW.js.map +0 -1
- package/dist/chunk-M4JOF35U.js.map +0 -1
- package/dist/chunk-QTBMLMKT.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spill-store.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Buffer } from 'node:buffer'\nimport { createHash, randomBytes } from 'node:crypto'\nimport { posix } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill'\nimport type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'\nimport { ensureDirPath } from './spill.ts'\n\n/** The workspace directory spill artifacts land under when none is configured. */\nconst DEFAULT_SPILL_DIR = '/tmp/dsh-spill'\n\n/** Bytes of randomness prefixed to a spill file name; 12 hex characters. */\nconst NAME_PREFIX_BYTES = 6\n\n/** Hex characters of the session-id digest that names a session's directory. */\nconst SESSION_DIGEST_CHARS = 12\n\n/** Configuration for the mirage spill store. */\nexport interface MirageSpillConfig {\n /**\n * Workspace directory the artifacts are written under, one\n * subdirectory per session. Absolute, and normalized before use; must\n * fall inside a mount this world can write, and the default sits on\n * the `/tmp` ram mount the bundle patch provides.\n */\n dir?: string\n}\n\n/**\n * Encode one caller-supplied string as a single safe path segment.\n *\n * `suggestedName` is a hint from a tool, never a path: it may be empty,\n * may spell `..`, and may carry separators or control characters. Only\n * the portable filename characters survive as themselves; everything\n * else, `~` included, becomes `~XXXX` so the encoding stays reversible\n * and cannot collide with a name that was already safe.\n *\n * @param raw the caller's suggested name.\n * @returns one path segment containing no separator and no traversal.\n */\nexport function encodeSegment(raw: string): string {\n if (raw.length === 0) return '~'\n // `.` is a portable filename character, so the loop below would pass\n // both of these through unchanged and hand a traversal to the join.\n if (raw === '.') return '~002E'\n if (raw === '..') return '~002E~002E'\n let out = ''\n for (const ch of raw) {\n const code = ch.codePointAt(0) ?? 0\n out +=\n ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)\n ? ch\n : `~${code.toString(16).toUpperCase().padStart(4, '0')}`\n }\n return out\n}\n\n/**\n * The directory name one session's artifacts share.\n *\n * The id is digested rather than spelled out: a session id is an\n * identifier the host chose, and a directory listing an agent can read\n * is no place to republish it.\n *\n * @param sessionId the owning session's id.\n * @returns a single path segment naming that session's directory.\n */\nexport function sessionDirName(sessionId: string): string {\n const digest = createHash('sha256').update(sessionId).digest('hex')\n return `session-${digest.slice(0, SESSION_DIGEST_CHARS)}`\n}\n\n/**\n * Mirage-backed implementation of `ctx.spillStore`.\n *\n * The spill policy replaces an oversized tool result with a preview and\n * a locator, telling the model to read or grep it for the rest. That\n * only works if the locator names a file the model can reach, and in a\n * mirage world the model reaches the workspace, not the host: a spill\n * written to the harness's own disk hands it a path its next `grep`\n * cannot open, which reads as output the harness lost. So the artifact\n * is written through the same dispatcher every other effect in this world\n * passes, and the locator is an ordinary workspace path.\n *\n * The seam asks for a private location. mirage has no permission bits\n * to offer: the workspace is the boundary, and a deployment that needs\n * the artifacts out of the agent's reach puts `dir` on a mount the\n * session has no grant for.\n */\nexport class MirageSpillStore extends SpillStore {\n static readonly inject = ['mirage']\n\n private readonly dir: string\n\n constructor(ctx: Context, config: MirageSpillConfig = {}) {\n super(ctx)\n const dir = config.dir ?? DEFAULT_SPILL_DIR\n if (!posix.isAbsolute(dir)) {\n throw new Error(`mirage: spill dir must be an absolute workspace path, got ${dir}`)\n }\n // Canonical once, here, rather than per save: a `.` or `..` left in\n // the path is written literally by the dispatcher and normalized away\n // by everything that reads a locator back, so the artifact lands on\n // a key neither the shell nor the fs seam can address. That is the\n // dead locator this store exists to prevent.\n this.dir = posix.resolve('/', dir)\n }\n\n async saveText(input: SaveTextSpill): Promise<SpillRef> {\n const workspace = await this.ctx.mirage.ready\n const dir = posix.join(this.dir, sessionDirName(input.owner.sessionId))\n // Derived from the suggested name, never equal to it: the random\n // prefix is what keeps two results with one suggested name apart.\n const name = `${randomBytes(NAME_PREFIX_BYTES).toString('hex')}-${encodeSegment(input.suggestedName)}`\n const path = posix.join(dir, name)\n try {\n await ensureDirPath(\n { exists: (p) => workspace.vfs.exists(p), mkdir: (p) => workspace.vfs.mkdir(p) },\n dir,\n )\n await workspace.vfs.write(path, input.content)\n } catch (err) {\n // Rejecting is the contract: the spill policy keeps the inline\n // result on a failure, which is a bounded answer, where a locator\n // pointing at a file that was never written is a dead end the\n // model cannot tell from a real one.\n throw new Error(`mirage: cannot write spill artifact to ${path}`, { cause: err })\n }\n return {\n locator: SpillLocator(path),\n bytes: Buffer.byteLength(input.content, 'utf8'),\n retrievalHint: 'Use read with offset/limit, or grep this workspace path to search within it.',\n }\n }\n}\n"],"mappings":";;;;;AAcA,SAAS,cAAc;AACvB,SAAS,YAAY,mBAAmB;AACxC,SAAS,aAAa;AAEtB,SAAS,cAAc,kBAAkB;AAKzC,IAAM,oBAAoB;AAG1B,IAAM,oBAAoB;AAG1B,IAAM,uBAAuB;AAyBtB,SAAS,cAAc,KAAqB;AACjD,MAAI,IAAI,WAAW,EAAG,QAAO;AAG7B,MAAI,QAAQ,IAAK,QAAO;AACxB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,MAAM;AACV,aAAW,MAAM,KAAK;AACpB,UAAM,OAAO,GAAG,YAAY,CAAC,KAAK;AAClC,WACE,OAAO,OAAO,mBAAmB,KAAK,EAAE,IACpC,KACA,IAAI,KAAK,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAC5D;AACA,SAAO;AACT;AAYO,SAAS,eAAe,WAA2B;AACxD,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAClE,SAAO,WAAW,OAAO,MAAM,GAAG,oBAAoB,CAAC;AACzD;AAmBO,IAAM,mBAAN,cAA+B,WAAW;AAAA,EAC/C,OAAgB,SAAS,CAAC,QAAQ;AAAA,EAEjB;AAAA,EAEjB,YAAY,KAAc,SAA4B,CAAC,GAAG;AACxD,UAAM,GAAG;AACT,UAAM,MAAM,OAAO,OAAO;AAC1B,QAAI,CAAC,MAAM,WAAW,GAAG,GAAG;AAC1B,YAAM,IAAI,MAAM,6DAA6D,GAAG,EAAE;AAAA,IACpF;AAMA,SAAK,MAAM,MAAM,QAAQ,KAAK,GAAG;AAAA,EACnC;AAAA,EAEA,MAAM,SAAS,OAAyC;AACtD,UAAM,YAAY,MAAM,KAAK,IAAI,OAAO;AACxC,UAAM,MAAM,MAAM,KAAK,KAAK,KAAK,eAAe,MAAM,MAAM,SAAS,CAAC;AAGtE,UAAM,OAAO,GAAG,YAAY,iBAAiB,EAAE,SAAS,KAAK,CAAC,IAAI,cAAc,MAAM,aAAa,CAAC;AACpG,UAAM,OAAO,MAAM,KAAK,KAAK,IAAI;AACjC,QAAI;AACF,YAAM;AAAA,QACJ,EAAE,QAAQ,CAAC,MAAM,UAAU,IAAI,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,UAAU,IAAI,MAAM,CAAC,EAAE;AAAA,QAC/E;AAAA,MACF;AACA,YAAM,UAAU,IAAI,MAAM,MAAM,MAAM,OAAO;AAAA,IAC/C,SAAS,KAAK;AAKZ,YAAM,IAAI,MAAM,0CAA0C,IAAI,IAAI,EAAE,OAAO,IAAI,CAAC;AAAA,IAClF;AACA,WAAO;AAAA,MACL,SAAS,aAAa,IAAI;AAAA,MAC1B,OAAO,OAAO,WAAW,MAAM,SAAS,MAAM;AAAA,MAC9C,eAAe;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -6,18 +6,24 @@ type SandboxPolicy = Parameters<FileSystem['writeText']>[4];
|
|
|
6
6
|
interface MirageFsConfig {
|
|
7
7
|
/** Virtual base directory for relative paths. Defaults to `/`. */
|
|
8
8
|
cwd?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Read and write as this named workspace session, so the profile
|
|
11
|
+
* that confines the session's shell confines its file tools too. The
|
|
12
|
+
* workspace's default session otherwise.
|
|
13
|
+
*/
|
|
14
|
+
sessionId?: string;
|
|
9
15
|
/** Exclusive byte limit on each overwrite-diff side. Defaults to 10 MiB. */
|
|
10
16
|
diffBasisMaxBytes?: number;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
19
|
* Mirage-backed implementation of `ctx.fs`. Targets are canonical virtual
|
|
14
20
|
* paths (namespace symlinks followed), every operation walks the workspace
|
|
15
|
-
*
|
|
21
|
+
* dispatcher — session grants, admission policies, cache read-through and
|
|
16
22
|
* post-write invalidation all fire exactly as they do for a shell command —
|
|
17
23
|
* and `processPath` answers in the same virtual path space the mirage shell
|
|
18
24
|
* executes in, so the two providers share one execution world.
|
|
19
25
|
*
|
|
20
|
-
* One limit worth stating: mirage's
|
|
26
|
+
* One limit worth stating: mirage's `ws.vfs` takes no `AbortSignal`, so
|
|
21
27
|
* cancellation is honored at this adapter's own boundaries (before a
|
|
22
28
|
* dispatch, between listing entries) and not inside a single op. A long
|
|
23
29
|
* read from a remote backend therefore runs to completion after the
|
|
@@ -27,11 +33,18 @@ declare class MirageFileSystem extends FileSystem {
|
|
|
27
33
|
static readonly inject: string[];
|
|
28
34
|
private fsOps;
|
|
29
35
|
private readonly cwd;
|
|
36
|
+
private readonly sessionId;
|
|
30
37
|
private readonly diffBasisMaxBytes;
|
|
31
38
|
private readonly locks;
|
|
32
39
|
constructor(ctx: Context, config?: MirageFsConfig);
|
|
33
|
-
private
|
|
34
|
-
|
|
40
|
+
private files;
|
|
41
|
+
/**
|
|
42
|
+
* The namespace as the session this adapter's ops run as sees it: the
|
|
43
|
+
* dispatcher's reader, so a link the session cannot see is never followed
|
|
44
|
+
* (the typed path reaches the dispatcher and is refused as absent, not
|
|
45
|
+
* resolved to the visible target it points at), and never listed.
|
|
46
|
+
*/
|
|
47
|
+
private get reader();
|
|
35
48
|
/**
|
|
36
49
|
* The same confinement claim `MirageShellExecutor` makes, off the same
|
|
37
50
|
* fact and for the same reason: with every runtime reaching only the
|
|
@@ -82,6 +95,22 @@ declare class MirageFileSystem extends FileSystem {
|
|
|
82
95
|
signal?: AbortSignal;
|
|
83
96
|
}): Promise<FsTarget>;
|
|
84
97
|
processPath(target: FsTarget): string;
|
|
98
|
+
/**
|
|
99
|
+
* The workspace path naming the same file as `hostPath`, when a mount
|
|
100
|
+
* puts one there.
|
|
101
|
+
*
|
|
102
|
+
* Most of this world has no host spelling at all: an S3 key, a Slack
|
|
103
|
+
* message and a Notion row are not files the harness could open, and
|
|
104
|
+
* claiming otherwise would hand the caller a path that reads as the
|
|
105
|
+
* wrong bytes. A disk mount is the one place the two worlds hold the
|
|
106
|
+
* same file, so it is the one place a host path is answerable; every
|
|
107
|
+
* other mount declines, and so does a workspace still building.
|
|
108
|
+
*
|
|
109
|
+
* @param hostPath absolute path in the harness host filesystem.
|
|
110
|
+
* @returns the virtual path for the same file, or undefined when no
|
|
111
|
+
* mount maps it.
|
|
112
|
+
*/
|
|
113
|
+
processPathFromHostPath(hostPath: string): string | undefined;
|
|
85
114
|
fileUrl(target: FsTarget): string;
|
|
86
115
|
contains(parent: FsTarget, child: FsTarget): boolean;
|
|
87
116
|
stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
|
|
@@ -90,7 +119,22 @@ declare class MirageFileSystem extends FileSystem {
|
|
|
90
119
|
}, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
|
|
91
120
|
readText(target: FsTarget, signal?: AbortSignal): Promise<string>;
|
|
92
121
|
streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>;
|
|
122
|
+
/**
|
|
123
|
+
* Stat a read target and refuse anything that is not a regular file.
|
|
124
|
+
*
|
|
125
|
+
* A backend read of a directory key surfaces as a missing path, which
|
|
126
|
+
* would report a path that plainly exists as absent.
|
|
127
|
+
*
|
|
128
|
+
* @param target the resolved target about to be read.
|
|
129
|
+
* @param signal aborts the stat.
|
|
130
|
+
* @returns the stat row, or undefined when the mount reports none.
|
|
131
|
+
*/
|
|
132
|
+
private statRegularFile;
|
|
93
133
|
readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
|
|
134
|
+
readByteRange(target: FsTarget, range: {
|
|
135
|
+
offset: number;
|
|
136
|
+
length: number;
|
|
137
|
+
}, signal?: AbortSignal): Promise<Uint8Array>;
|
|
94
138
|
listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>;
|
|
95
139
|
private dirEntry;
|
|
96
140
|
writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxPolicy): Promise<FsWriteOutcome>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
export { M as MirageConfig, a as MirageMount, b as MirageMountBlock, c as MirageRuntimeBlock, d as MirageService } from './service-
|
|
2
|
-
export { M as MirageFileSystem, a as MirageFsConfig } from './fs-
|
|
3
|
-
export { M as
|
|
1
|
+
export { M as MirageConfig, a as MirageMount, b as MirageMountBlock, c as MirageRuntimeBlock, d as MirageService } from './service-vCjLPYMI.js';
|
|
2
|
+
export { M as MirageFileSystem, a as MirageFsConfig } from './fs-BNOIFi9o.js';
|
|
3
|
+
export { M as MirageSubprocess, a as MirageSubprocessConfig } from './subprocess-CInesMpR.js';
|
|
4
|
+
export { M as MirageShellConfig, a as MirageShellExecutor } from './shell-BAyBocjm.js';
|
|
5
|
+
export { M as MirageSpillConfig, a as MirageSpillStore } from './spill-store-BfljgRi0.js';
|
|
4
6
|
import { Context } from '@deepseek-ai/cordis';
|
|
5
7
|
import { AskHandler } from '@struktoai/mirage-core/policy/decisions';
|
|
6
8
|
import { Decision } from '@struktoai/mirage-core/policy/types';
|
|
9
|
+
import '@struktoai/mirage-core/workspace/mount/spec';
|
|
7
10
|
import '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
8
11
|
import '@struktoai/mirage-node';
|
|
9
12
|
import '@deepseek-ai/dsh-fs';
|
|
13
|
+
import '@deepseek-ai/dsh-subprocess';
|
|
10
14
|
import '@deepseek-ai/dsh-shell';
|
|
15
|
+
import '@deepseek-ai/dsh-spill';
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* The tool an approval request is attributed to. A mirage ask is raised
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MirageFileSystem
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FRF6WPLN.js";
|
|
4
|
+
import {
|
|
5
|
+
MirageShellExecutor
|
|
6
|
+
} from "./chunk-KUIV2Z3M.js";
|
|
7
|
+
import {
|
|
8
|
+
MirageSpillStore
|
|
9
|
+
} from "./chunk-T4BSJAGN.js";
|
|
10
|
+
import "./chunk-QH5H3VND.js";
|
|
11
|
+
import {
|
|
12
|
+
MirageSubprocess
|
|
13
|
+
} from "./chunk-G2GHA2F6.js";
|
|
4
14
|
import {
|
|
5
15
|
APPROVAL_TOOL_NAME,
|
|
6
16
|
MirageService,
|
|
7
17
|
approvalReason,
|
|
8
18
|
approverOf,
|
|
9
19
|
askThroughApproval
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import
|
|
12
|
-
MirageShellExecutor
|
|
13
|
-
} from "./chunk-QTBMLMKT.js";
|
|
14
|
-
import "./chunk-2F2PJ7D7.js";
|
|
20
|
+
} from "./chunk-OHZACFWO.js";
|
|
21
|
+
import "./chunk-OZ2F4R6T.js";
|
|
15
22
|
export {
|
|
16
23
|
APPROVAL_TOOL_NAME,
|
|
17
24
|
MirageFileSystem,
|
|
18
25
|
MirageService,
|
|
19
26
|
MirageShellExecutor,
|
|
27
|
+
MirageSpillStore,
|
|
28
|
+
MirageSubprocess,
|
|
20
29
|
approvalReason,
|
|
21
30
|
approverOf,
|
|
22
31
|
askThroughApproval
|
package/dist/plugin/fs.d.ts
CHANGED
package/dist/plugin/fs.js
CHANGED
package/dist/plugin/service.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { d as default } from '../service-
|
|
1
|
+
export { d as default } from '../service-vCjLPYMI.js';
|
|
2
2
|
import '@deepseek-ai/cordis';
|
|
3
3
|
import '@struktoai/mirage-core/policy/types';
|
|
4
|
+
import '@struktoai/mirage-core/workspace/mount/spec';
|
|
4
5
|
import '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
5
6
|
import '@struktoai/mirage-node';
|
package/dist/plugin/service.js
CHANGED
package/dist/plugin/shell.d.ts
CHANGED
package/dist/plugin/shell.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { M as default } from '../subprocess-CInesMpR.js';
|
|
2
|
+
import '@deepseek-ai/cordis';
|
|
3
|
+
import '@deepseek-ai/dsh-subprocess';
|
|
4
|
+
import '../service-vCjLPYMI.js';
|
|
5
|
+
import '@struktoai/mirage-core/policy/types';
|
|
6
|
+
import '@struktoai/mirage-core/workspace/mount/spec';
|
|
7
|
+
import '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
8
|
+
import '@struktoai/mirage-node';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Service, Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import {
|
|
2
|
+
import { ShellExplanation } from '@struktoai/mirage-core/policy/types';
|
|
3
|
+
import { Mount } from '@struktoai/mirage-core/workspace/mount/spec';
|
|
3
4
|
import { MountSpec } from '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
4
|
-
import { Workspace,
|
|
5
|
+
import { Workspace, NodeWorkspaceOptions } from '@struktoai/mirage-node';
|
|
5
6
|
|
|
6
7
|
declare module '@deepseek-ai/cordis' {
|
|
7
8
|
interface Context {
|
|
@@ -9,28 +10,30 @@ declare module '@deepseek-ai/cordis' {
|
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* A declarative mount, YAML-friendly: the
|
|
13
|
-
* registry type and built through `
|
|
13
|
+
* A declarative mount, YAML-friendly: the VFS is named by its
|
|
14
|
+
* registry type and built through `buildVfs`, so a dsh bundle
|
|
14
15
|
* patch can mount anything without holding a live instance. Told apart
|
|
15
|
-
* from a live `Mount` by the type of `
|
|
16
|
+
* from a live `Mount` by the type of `vfs`: a string names a
|
|
16
17
|
* registry entry, an object is the instance itself.
|
|
17
18
|
*/
|
|
18
19
|
interface MirageMountBlock {
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/** VFS registry name (`ram`, `s3`, `slack`, `redis`, ...). */
|
|
21
|
+
vfs: string;
|
|
21
22
|
/** Mount mode: `read`, `write`, or `exec`. Omitted = the workspace default. */
|
|
22
23
|
mode?: string;
|
|
23
|
-
/**
|
|
24
|
+
/** VFS constructor config, passed to the registry factory. */
|
|
24
25
|
config?: Record<string, unknown>;
|
|
25
26
|
}
|
|
26
27
|
/** One mount entry: a live spec or instance, or a declarative block. */
|
|
27
28
|
type MirageMount = MountSpec | Mount | MirageMountBlock;
|
|
28
|
-
/** A runtime entry, YAML-friendly: a name, or a name with
|
|
29
|
+
/** A runtime entry, YAML-friendly: a name, or a name with its options. */
|
|
29
30
|
interface MirageRuntimeBlock {
|
|
30
31
|
/** Runtime registry name (`monty`, `pyodide`, `quickjs`, ...). */
|
|
31
32
|
name: string;
|
|
32
33
|
/** Command words this runtime captures (e.g. `['python', 'python3']`). */
|
|
33
34
|
captures?: string[];
|
|
35
|
+
/** The runtime's own config block; a key it does not have is refused. */
|
|
36
|
+
config?: Record<string, unknown>;
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
36
39
|
* Configuration for the shared mirage execution world. Exactly one of
|
|
@@ -80,8 +83,8 @@ interface MirageConfig {
|
|
|
80
83
|
* which is what keeps `ctx.fs` targets and `ctx.shell` commands in one
|
|
81
84
|
* execution world: a `processPath` handed to the shell resolves there.
|
|
82
85
|
*
|
|
83
|
-
* Construction with live
|
|
84
|
-
* block defers to `
|
|
86
|
+
* Construction with live mounts is synchronous; a declarative mount
|
|
87
|
+
* block defers to `buildVfs`, so consumers await `ready` (the
|
|
85
88
|
* adapters do) and `workspace` throws until it resolves, mirroring how
|
|
86
89
|
* the E2B service owner exposes its sandbox.
|
|
87
90
|
*/
|
|
@@ -94,6 +97,15 @@ declare class MirageService extends Service {
|
|
|
94
97
|
constructor(ctx: Context, config?: MirageConfig);
|
|
95
98
|
/** The live workspace; throws until `ready` resolves. */
|
|
96
99
|
get workspace(): Workspace;
|
|
100
|
+
/**
|
|
101
|
+
* The live workspace, or null while it is still building.
|
|
102
|
+
*
|
|
103
|
+
* The non-throwing twin of {@link workspace}, for a synchronous seam
|
|
104
|
+
* method whose honest answer before `ready` is "this world cannot tell
|
|
105
|
+
* you" rather than an exception. Reads `built` directly, as
|
|
106
|
+
* {@link vfsOnly} does.
|
|
107
|
+
*/
|
|
108
|
+
get workspaceIfReady(): Workspace | null;
|
|
97
109
|
/**
|
|
98
110
|
* The decision ledger: every ask this world has raised and every
|
|
99
111
|
* answer given, questions still waiting included.
|
|
@@ -106,9 +118,9 @@ declare class MirageService extends Service {
|
|
|
106
118
|
*/
|
|
107
119
|
get decisions(): Workspace['decisions'];
|
|
108
120
|
/**
|
|
109
|
-
* What a line would do here, without doing it:
|
|
110
|
-
*
|
|
111
|
-
*
|
|
121
|
+
* What a line would do here, without doing it: the line's verdict, with
|
|
122
|
+
* the exact exit code and stderr the agent would get, and its parse tree,
|
|
123
|
+
* every command carrying its own outcome and the policies that spoke.
|
|
112
124
|
*
|
|
113
125
|
* A dry run through the same gate the dispatcher uses, so a host
|
|
114
126
|
* reading this and an agent typing the line cannot be told different
|
|
@@ -119,9 +131,9 @@ declare class MirageService extends Service {
|
|
|
119
131
|
*
|
|
120
132
|
* @param line the command line to judge.
|
|
121
133
|
* @param sessionId the session to judge it in; the default when empty.
|
|
122
|
-
* @returns
|
|
134
|
+
* @returns the line's explanation.
|
|
123
135
|
*/
|
|
124
|
-
explain(line: string, sessionId?: string): Promise<
|
|
136
|
+
explain(line: string, sessionId?: string): Promise<ShellExplanation>;
|
|
125
137
|
/**
|
|
126
138
|
* True while every runtime in the world reaches only the vfs
|
|
127
139
|
* (`Runtime.reach`), meaning the workspace dispatch is the single
|
|
@@ -132,7 +144,7 @@ declare class MirageService extends Service {
|
|
|
132
144
|
* sandbox can act around the gate. Answers before `ready` from the
|
|
133
145
|
* constructor-resolved entries (mounts resolve asynchronously,
|
|
134
146
|
* runtimes never do) and from the live workspace afterwards, so a
|
|
135
|
-
* later `addRuntime` is seen.
|
|
147
|
+
* later `addRuntime` or `removeRuntime` is seen.
|
|
136
148
|
*/
|
|
137
149
|
get vfsOnly(): boolean;
|
|
138
150
|
private open;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import { ShellExecutor, ShellExecRequest, ShellExecSpec,
|
|
2
|
+
import { ShellExecutor, ShellExecRequest, ShellExecSpec, ShellExecution } from '@deepseek-ai/dsh-shell';
|
|
3
3
|
|
|
4
4
|
/** Configuration for the mirage shell executor. */
|
|
5
5
|
interface MirageShellConfig {
|
|
@@ -75,6 +75,9 @@ declare class MirageShellExecutor extends ShellExecutor {
|
|
|
75
75
|
private readonly spillDir;
|
|
76
76
|
private sessionReady;
|
|
77
77
|
private readOnlyReady;
|
|
78
|
+
private readonly seeding;
|
|
79
|
+
private issued;
|
|
80
|
+
private readonly seeded;
|
|
78
81
|
constructor(ctx: Context, config?: MirageShellConfig);
|
|
79
82
|
private workspace;
|
|
80
83
|
/**
|
|
@@ -101,7 +104,7 @@ declare class MirageShellExecutor extends ShellExecutor {
|
|
|
101
104
|
* workspace-write sandbox: reads and writes land only where mounts
|
|
102
105
|
* (and their modes) allow. Declaring it lets sandbox-aware plugins
|
|
103
106
|
* (dsh's permission presets) compose over this executor. A world
|
|
104
|
-
* holding a runtime with
|
|
107
|
+
* holding a runtime with entry points around the gate (the host `local`
|
|
105
108
|
* python, a remote sandbox) voids that claim, so this answers
|
|
106
109
|
* undefined then (the base contract's "does not sandbox") and those
|
|
107
110
|
* plugins refuse to compose instead of trusting a lie.
|
|
@@ -145,36 +148,6 @@ declare class MirageShellExecutor extends ShellExecutor {
|
|
|
145
148
|
* @returns the facts to stamp, or undefined when nothing is claimed.
|
|
146
149
|
*/
|
|
147
150
|
private sandboxInfo;
|
|
148
|
-
/**
|
|
149
|
-
* The retention budget of a background command's console.
|
|
150
|
-
*
|
|
151
|
-
* Only the background path caps retention: there a follow loop drains
|
|
152
|
-
* the console while the command still runs, so the budget bounds what
|
|
153
|
-
* the loop has not reached yet, and a chunk lost to it is reported as
|
|
154
|
-
* a gap in the sequence. A foreground console is read once, after the
|
|
155
|
-
* fact, with nothing to bound.
|
|
156
|
-
*
|
|
157
|
-
* @param spec the resolved spec carrying the stdout budget.
|
|
158
|
-
* @returns the retention budget in bytes.
|
|
159
|
-
*/
|
|
160
|
-
private retentionFor;
|
|
161
|
-
/**
|
|
162
|
-
* Take a finished run's two streams off its console, capped, and spill
|
|
163
|
-
* whichever one lost bytes.
|
|
164
|
-
*
|
|
165
|
-
* A foreground run streams into a console rather than returning its
|
|
166
|
-
* output whole, because mirage throws on abort: bytes a killed command
|
|
167
|
-
* had already printed are recoverable from a sink and nowhere else.
|
|
168
|
-
* Only a truncated stream spills, since an untruncated one is already
|
|
169
|
-
* whole in `text` and writing a file for it would put a copy of every
|
|
170
|
-
* command's output on a mount. The console this reads is untrimmed, so
|
|
171
|
-
* a spill is the whole stream rather than the tail of one.
|
|
172
|
-
*
|
|
173
|
-
* @param console_ the console this run streamed into.
|
|
174
|
-
* @param spec the resolved spec carrying the stdout budget.
|
|
175
|
-
* @returns the capped streams, each with a spill path when it lost bytes.
|
|
176
|
-
*/
|
|
177
|
-
private collectFrom;
|
|
178
151
|
/**
|
|
179
152
|
* Whether this run was refused, by the session's permission document
|
|
180
153
|
* or by the read-only narrowing.
|
|
@@ -214,7 +187,7 @@ declare class MirageShellExecutor extends ShellExecutor {
|
|
|
214
187
|
*
|
|
215
188
|
* @param ws the live workspace holding the session.
|
|
216
189
|
* @param sessionId the session this call runs in.
|
|
217
|
-
* @param
|
|
190
|
+
* @param managed the call's managed snapshot.
|
|
218
191
|
*/
|
|
219
192
|
private applyManagedEnv;
|
|
220
193
|
private readOnlySession;
|
|
@@ -251,8 +224,25 @@ declare class MirageShellExecutor extends ShellExecutor {
|
|
|
251
224
|
*/
|
|
252
225
|
private provisionReadOnly;
|
|
253
226
|
private provisionSession;
|
|
254
|
-
|
|
255
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Where and as whom one command runs, settled before it starts: the
|
|
229
|
+
* session binding, the workdir in this world, and its managed env.
|
|
230
|
+
*
|
|
231
|
+
* Seeding the env is the one step that writes, so it comes last and
|
|
232
|
+
* waits for any seed already running on the same session (the bound
|
|
233
|
+
* one and its read-only twin queue apart). Calls are numbered as they
|
|
234
|
+
* arrive, and a seed is skipped once a later call has seeded the same
|
|
235
|
+
* session, so the newest snapshot wins: a slow or abandoned preparation
|
|
236
|
+
* never lands an old one over it, and a stall before the seed holds up
|
|
237
|
+
* no other call. A call carrying no snapshot seeds nothing, so it
|
|
238
|
+
* never counts as the newest, but it still waits for the seeds already
|
|
239
|
+
* running, so its command never sees one half applied.
|
|
240
|
+
*
|
|
241
|
+
* @param spec the resolved spec being prepared.
|
|
242
|
+
* @returns the workspace, session and workdir the command runs under.
|
|
243
|
+
*/
|
|
244
|
+
private prepare;
|
|
245
|
+
execute(spec: ShellExecSpec): Promise<ShellExecution>;
|
|
256
246
|
}
|
|
257
247
|
|
|
258
248
|
export { type MirageShellConfig as M, MirageShellExecutor as a };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { SpillStore, SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill';
|
|
3
|
+
|
|
4
|
+
/** Configuration for the mirage spill store. */
|
|
5
|
+
interface MirageSpillConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Workspace directory the artifacts are written under, one
|
|
8
|
+
* subdirectory per session. Absolute, and normalized before use; must
|
|
9
|
+
* fall inside a mount this world can write, and the default sits on
|
|
10
|
+
* the `/tmp` ram mount the bundle patch provides.
|
|
11
|
+
*/
|
|
12
|
+
dir?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mirage-backed implementation of `ctx.spillStore`.
|
|
16
|
+
*
|
|
17
|
+
* The spill policy replaces an oversized tool result with a preview and
|
|
18
|
+
* a locator, telling the model to read or grep it for the rest. That
|
|
19
|
+
* only works if the locator names a file the model can reach, and in a
|
|
20
|
+
* mirage world the model reaches the workspace, not the host: a spill
|
|
21
|
+
* written to the harness's own disk hands it a path its next `grep`
|
|
22
|
+
* cannot open, which reads as output the harness lost. So the artifact
|
|
23
|
+
* is written through the same dispatcher every other effect in this world
|
|
24
|
+
* passes, and the locator is an ordinary workspace path.
|
|
25
|
+
*
|
|
26
|
+
* The seam asks for a private location. mirage has no permission bits
|
|
27
|
+
* to offer: the workspace is the boundary, and a deployment that needs
|
|
28
|
+
* the artifacts out of the agent's reach puts `dir` on a mount the
|
|
29
|
+
* session has no grant for.
|
|
30
|
+
*/
|
|
31
|
+
declare class MirageSpillStore extends SpillStore {
|
|
32
|
+
static readonly inject: string[];
|
|
33
|
+
private readonly dir;
|
|
34
|
+
constructor(ctx: Context, config?: MirageSpillConfig);
|
|
35
|
+
saveText(input: SaveTextSpill): Promise<SpillRef>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { type MirageSpillConfig as M, MirageSpillStore as a };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { SubprocessRuntime, SubprocessSpawnSpec, SubprocessHandle, SubprocessTerminalEnvironment, SubprocessTerminalSpawnSpec, SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess';
|
|
3
|
+
import './service-vCjLPYMI.js';
|
|
4
|
+
|
|
5
|
+
interface MirageSubprocessConfig {
|
|
6
|
+
/** Existing workspace session; omitted selects the default session. */
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
}
|
|
9
|
+
/** One workspace execution world. No host PATH, native descendants, or PTY claims. */
|
|
10
|
+
declare class MirageSubprocess extends SubprocessRuntime {
|
|
11
|
+
private readonly config;
|
|
12
|
+
static readonly inject: string[];
|
|
13
|
+
private readonly handles;
|
|
14
|
+
private closed;
|
|
15
|
+
constructor(ctx: Context, config?: MirageSubprocessConfig);
|
|
16
|
+
resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
|
|
17
|
+
spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
|
|
18
|
+
terminalEnvironment(signal?: AbortSignal): Promise<SubprocessTerminalEnvironment>;
|
|
19
|
+
spawnTerminal(_spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
|
|
20
|
+
private checkWorld;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { MirageSubprocess as M, type MirageSubprocessConfig as a };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struktoai/mirage-dsh",
|
|
3
|
-
"version": "0.0.7
|
|
4
|
-
"description": "DeepSeek Harness (dsh) providers backed by a mirage workspace: ctx.fs and ctx.shell over
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "DeepSeek Harness (dsh) providers backed by a mirage workspace: ctx.fs and ctx.shell over mounts",
|
|
5
5
|
"homepage": "https://github.com/strukto-ai/mirage#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/strukto-ai/mirage/issues"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": "
|
|
21
|
+
"node": "^22.20 || ^24.12 || >=25"
|
|
22
22
|
},
|
|
23
23
|
"main": "./dist/index.js",
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"./shell": {
|
|
39
39
|
"types": "./dist/plugin/shell.d.ts",
|
|
40
40
|
"import": "./dist/plugin/shell.js"
|
|
41
|
+
},
|
|
42
|
+
"./spill-store": {
|
|
43
|
+
"types": "./dist/plugin/spill-store.d.ts",
|
|
44
|
+
"import": "./dist/plugin/spill-store.js"
|
|
45
|
+
},
|
|
46
|
+
"./subprocess": {
|
|
47
|
+
"types": "./dist/plugin/subprocess.d.ts",
|
|
48
|
+
"import": "./dist/plugin/subprocess.js"
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
"dsh": {
|
|
@@ -50,21 +58,25 @@
|
|
|
50
58
|
"cordis.patch.yml"
|
|
51
59
|
],
|
|
52
60
|
"dependencies": {
|
|
53
|
-
"@struktoai/mirage-
|
|
54
|
-
"@struktoai/mirage-
|
|
61
|
+
"@struktoai/mirage-core": "0.0.7",
|
|
62
|
+
"@struktoai/mirage-node": "0.0.7"
|
|
55
63
|
},
|
|
56
64
|
"peerDependencies": {
|
|
57
|
-
"@deepseek-ai/cordis": "
|
|
58
|
-
"@deepseek-ai/dsh-fs": "0.
|
|
59
|
-
"@deepseek-ai/dsh-shell": "0.
|
|
65
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
66
|
+
"@deepseek-ai/dsh-fs": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-shell": "0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-spill": "0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2"
|
|
60
70
|
},
|
|
61
71
|
"devDependencies": {
|
|
62
|
-
"@deepseek-ai/cordis": "
|
|
63
|
-
"@deepseek-ai/dsh-fs": "0.
|
|
64
|
-
"@deepseek-ai/dsh-shell": "0.
|
|
72
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
73
|
+
"@deepseek-ai/dsh-fs": "0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-shell": "0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-spill": "0.2.0-rc.2",
|
|
65
76
|
"tsup": "^8.5.0",
|
|
66
77
|
"typescript": "^6.0.0",
|
|
67
|
-
"vitest": "^
|
|
78
|
+
"vitest": "^4.1.11",
|
|
79
|
+
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2"
|
|
68
80
|
},
|
|
69
81
|
"scripts": {
|
|
70
82
|
"build": "tsup",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/text.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\n\nconst BINARY_SAMPLE_BYTES = 8192\n\n// dsh text semantics: a NUL in the leading sample or invalid UTF-8 is a\n// binary file, refused as FS_NOT_TEXT rather than decoded lossily (the\n// mirage facade's readFileText decodes with fatal: false, which the dsh\n// seam contract forbids).\nexport function decodeStrictText(bytes: Uint8Array, displayPath: string): string {\n if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n throw new FsError(`cannot read \"${displayPath}\": binary file`, 'FS_NOT_TEXT')\n }\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n } catch (error: unknown) {\n throw new FsError(`cannot read \"${displayPath}\": invalid UTF-8 text`, 'FS_NOT_TEXT', {\n cause: error,\n })\n }\n}\n\nexport function normalizeLineEndings(value: string): string {\n return value.replaceAll('\\r\\n', '\\n')\n}\n\n// CRLF-majority sniff over a bounded sample, the same heuristic dsh's own\n// backends use to keep a rewritten file in its original line-ending style.\nexport function detectsCrlf(value: string): boolean {\n const sample = value.slice(0, 4096)\n const crlf = sample.split('\\r\\n').length - 1\n const lf = sample.split('\\n').length - 1 - crlf\n return crlf > lf\n}\n\nexport function restoreLineEndings(value: string, crlf: boolean): string {\n return crlf ? normalizeLineEndings(value).replaceAll('\\n', '\\r\\n') : value\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle.length === 0) return 0\n let count = 0\n let index = haystack.indexOf(needle)\n while (index !== -1) {\n count++\n index = haystack.indexOf(needle, index + needle.length)\n }\n return count\n}\n\n// Literal replacement with dsh's edit taxonomy: zero matches and an empty\n// oldString are FS_EDIT_NOT_FOUND, several matches without replaceAll is\n// FS_AMBIGUOUS_EDIT.\nexport function applyLiteralEdit(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n displayPath: string,\n): string {\n const matches = countOccurrences(content, oldString)\n if (matches === 0) {\n throw new FsError(`cannot edit \"${displayPath}\": oldString not found`, 'FS_EDIT_NOT_FOUND')\n }\n if (matches > 1 && !replaceAll) {\n throw new FsError(\n `cannot edit \"${displayPath}\": oldString matches ${String(matches)} locations; pass replaceAll or a longer unique string`,\n 'FS_AMBIGUOUS_EDIT',\n )\n }\n return replaceAll\n ? content.replaceAll(oldString, newString)\n : content.replace(oldString, newString)\n}\n\n// The first index at or after `from` that starts a UTF-8 sequence, so a tail\n// taken from there decodes as characters rather than as replacement marks.\nfunction charBoundary(bytes: Uint8Array, from: number): number {\n let start = from\n while (start < bytes.byteLength && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++\n return start\n}\n\n// Byte-accurate tail cap: keep the LAST maxBytes bytes of the encoded text,\n// re-aligned to a UTF-8 sequence boundary so the kept tail still decodes.\nexport function tailCap(text: string, maxBytes: number): { text: string; truncated: boolean } {\n const bytes = new TextEncoder().encode(text)\n if (bytes.byteLength <= maxBytes) return { text, truncated: false }\n const start = charBoundary(bytes, bytes.byteLength - maxBytes)\n return {\n text: new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(start)),\n truncated: true,\n }\n}\n\n/**\n * A bounded backlog of output bytes: the newest `budget` bytes, with the\n * oldest dropped as they overflow.\n *\n * Held as bytes rather than as a string because the string form had to\n * re-encode everything already buffered on every append to measure it,\n * which is quadratic in a command's output and lands on the same event\n * loop that serves the workspace. Appending here costs the chunk, not\n * the backlog.\n */\nexport class TailBuffer {\n private readonly budget: number\n private parts: Uint8Array[] = []\n private bytes = 0\n\n constructor(budget: number) {\n this.budget = budget\n }\n\n /**\n * Append a chunk, dropping the oldest bytes that no longer fit.\n *\n * @param data the bytes to append.\n * @returns true when bytes were dropped to make room.\n */\n append(data: Uint8Array): boolean {\n this.parts.push(data)\n this.bytes += data.byteLength\n if (this.bytes <= this.budget) return false\n let excess = this.bytes - this.budget\n while (excess > 0) {\n const head = this.parts[0]\n if (head === undefined) break\n if (head.byteLength <= excess) {\n this.parts.shift()\n excess -= head.byteLength\n this.bytes -= head.byteLength\n } else {\n this.parts[0] = head.subarray(excess)\n this.bytes -= excess\n excess = 0\n }\n }\n return true\n }\n\n /**\n * Drain everything held, decoded as text.\n *\n * The head may sit mid-character, since dropping is byte-exact, so it\n * is re-aligned before decoding rather than rendered as a replacement\n * mark.\n *\n * @returns the buffered text; the buffer is left empty.\n */\n take(): string {\n if (this.parts.length === 0) return ''\n const joined = new Uint8Array(this.bytes)\n let at = 0\n for (const part of this.parts) {\n joined.set(part, at)\n at += part.byteLength\n }\n this.parts = []\n this.bytes = 0\n return new TextDecoder('utf-8', { fatal: false }).decode(\n joined.subarray(charBoundary(joined, 0)),\n )\n }\n}\n"],"mappings":"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","names":[]}
|