{"record":{"id":"301ec1c97f738d75","repo":"agentscope-ai/agentscope","slug":"entry-path-r-declared-entry-size-bytes-but-sen","errorCode":null,"errorMessage":"{entry.path!r} declared {entry.size} bytes but sent {written}.","messagePattern":"(.+?) declared (.+?) bytes but sent (.+?)\\.","errorType":"validation","errorClass":"SkillUploadError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/_service/_workspace.py","lineNumber":478,"sourceCode":"        \"\"\"\n        for entry, upload in zip(manifest.entries, files):\n            info = tarfile.TarInfo(name=entry.path)\n            info.size = entry.size\n            info.mtime = 0\n            yield info.tobuf(tarfile.GNU_FORMAT)\n\n            written = 0\n            while chunk := await upload.read(_CHUNK_SIZE):\n                written += len(chunk)\n                if written > entry.size:\n                    raise SkillUploadError(\n                        f\"{entry.path!r} is larger than its declared \"\n                        f\"{entry.size} bytes.\",\n                    )\n                yield chunk\n\n            if written != entry.size:\n                raise SkillUploadError(\n                    f\"{entry.path!r} declared {entry.size} bytes but \"\n                    f\"sent {written}.\",\n                )\n            padding = -entry.size % _TAR_BLOCK\n            if padding:\n                yield b\"\\0\" * padding\n\n        # Two zero blocks mark the end of a tar archive.\n        yield b\"\\0\" * (2 * _TAR_BLOCK)\n\n    # ── Git status ─────────────────────────────────────────────────────\n\n    async def _read_git(\n        self,\n        backend: BackendBase,\n        cwd: str,\n    ) -> GitStatus | None:\n        \"\"\"Summarise the git state of ``cwd``, or return ``None``.","sourceCodeStart":460,"sourceCodeEnd":496,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/_service/_workspace.py#L460-L496","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Regenerate the manifest from the same files you stream, in the same operation","Check that the upload stream/file hasn't been truncated (compare stat().st_size with bytes actually yielded)","If sizes come from user input, validate size == actual byte count before starting the upload","For tests, make the fixture payload length match the declared size exactly"],"exampleFix":"# before\nentries = build_manifest(dir)  # sizes from earlier snapshot\nawait upload_skill(stream_from(dir, entries), ...)\n\n# after\nentries = build_manifest(dir)  # built immediately before streaming\nfor e in entries:\n    assert (dir / e.path).stat().st_size == e.size\nawait upload_skill(stream_from(dir, entries), ...)","handlingStrategy":"validation","validationCode":"def check_manifest_matches(base: Path, entries) -> None:\n    for e in entries:\n        p = base / e.path\n        if not p.is_file() or p.stat().st_size != e.size:\n            raise RuntimeError(f\"entry {e.path} is missing or truncated vs declared {e.size}\")","typeGuard":null,"tryCatchPattern":"try:\n    await upload_skill(...)\nexcept SkillUploadError as e:\n    if \"declared\" in str(e) and \"but sent\" in str(e):\n        raise  # stream ended early; regenerate manifest from current files before retrying","preventionTips":["Never reuse manifests across uploads; regenerate each time","Verify stream sources aren't truncated (compare yielded byte totals to stat sizes)","Avoid unit mismatches (bytes not KB) when computing entry sizes"],"tags":["tar","streaming","upload-validation","skill-upload"],"backgroundTag":"stream-size-mismatch","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}