agentscope-ai/agentscope · error · SkillUploadError

{entry.path!r} declared {entry.size} bytes but sent {written

Error message

{entry.path!r} declared {entry.size} bytes but sent {written}.

What it means

Raised by tar_stream when the total bytes read for a manifest entry do not exactly equal the declared entry.size (fewer bytes were sent). Because tar archives require exact per-entry lengths followed by block padding, a short read means either the source stream ended early or the declared size is too large. The upload is aborted with SkillUploadError before a corrupt archive is persisted.

Source

Thrown at src/agentscope/app/_service/_workspace.py:478

        """
        for entry, upload in zip(manifest.entries, files):
            info = tarfile.TarInfo(name=entry.path)
            info.size = entry.size
            info.mtime = 0
            yield info.tobuf(tarfile.GNU_FORMAT)

            written = 0
            while chunk := await upload.read(_CHUNK_SIZE):
                written += len(chunk)
                if written > entry.size:
                    raise SkillUploadError(
                        f"{entry.path!r} is larger than its declared "
                        f"{entry.size} bytes.",
                    )
                yield chunk

            if written != entry.size:
                raise SkillUploadError(
                    f"{entry.path!r} declared {entry.size} bytes but "
                    f"sent {written}.",
                )
            padding = -entry.size % _TAR_BLOCK
            if padding:
                yield b"\0" * padding

        # Two zero blocks mark the end of a tar archive.
        yield b"\0" * (2 * _TAR_BLOCK)

    # ── Git status ─────────────────────────────────────────────────────

    async def _read_git(
        self,
        backend: BackendBase,
        cwd: str,
    ) -> GitStatus | None:
        """Summarise the git state of ``cwd``, or return ``None``.

View on GitHub (pinned to e90f1c7592)

Solutions

  1. Regenerate the manifest from the same files you stream, in the same operation
  2. Check that the upload stream/file hasn't been truncated (compare stat().st_size with bytes actually yielded)
  3. If sizes come from user input, validate size == actual byte count before starting the upload
  4. For tests, make the fixture payload length match the declared size exactly

Example fix

# before
entries = build_manifest(dir)  # sizes from earlier snapshot
await upload_skill(stream_from(dir, entries), ...)

# after
entries = build_manifest(dir)  # built immediately before streaming
for e in entries:
    assert (dir / e.path).stat().st_size == e.size
await upload_skill(stream_from(dir, entries), ...)
Defensive patterns

Strategy: validation

Validate before calling

def check_manifest_matches(base: Path, entries) -> None:
    for e in entries:
        p = base / e.path
        if not p.is_file() or p.stat().st_size != e.size:
            raise RuntimeError(f"entry {e.path} is missing or truncated vs declared {e.size}")

Try / catch

try:
    await upload_skill(...)
except SkillUploadError as e:
    if "declared" in str(e) and "but sent" in str(e):
        raise  # stream ended early; regenerate manifest from current files before retrying

Prevention

When it happens

Trigger: Calling upload_skill where the manifest declares entry.size larger than what the underlying stream yields — e.g. the file was truncated after the manifest was built, an async stream terminated early, or the manifest was constructed with sizes from a different directory snapshot. Also triggered deliberately by tests like test_declared_size_is_verified feeding fewer bytes than declared.

Common situations: File shrunk between stat() and read() (log rotation, temp file cleanup), wrong units (KB vs bytes) when computing sizes, streaming from a generator that stops early, stale manifest reused across uploads.

Related errors


AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28). Data as JSON: /api/errors/301ec1c97f738d75. Report an issue: GitHub.