diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -230,6 +230,11 @@ How image exports (PNG 2400×1600, SVG, PDF) are framed: `convert` fits the mode
 rectangle, centred and scaled without distortion; `--view extents` or `--view current` forces
 either framing. To frame a selection set, `zoom {"mode":"object"}` after selecting.
 
+When reading a file leaves something out (records a DXF reader can't place, object records of a
+damaged DWG), `info` reports it (`skipped`, `readNotes`), `convert` warns on stderr and `run`
+prints the DWG notes with its log; the exit code stays 0. MCP `open` replies with the notes as
+`warnings`.
+
 MCP tools include `command_line` (type at the prompt), `execute` (any command with JSON),
 `inspect_drawing`, `query_entities`, `render` (returns a PNG) and, when connected to the app,
 `screenshot` and `ui_click`. The desktop app's JSON control channel is documented in
diff --git a/apps/cadcraft-cli/src/main.rs b/apps/cadcraft-cli/src/main.rs
--- a/apps/cadcraft-cli/src/main.rs
+++ b/apps/cadcraft-cli/src/main.rs
@@ -286,7 +286,8 @@ fn convert(args: &[String]) -> Result<(), String> {
     check_framed_output(framing, Some(o))?;
     let mut s = Session::empty();
     open(&mut s, i)?;
-    if let Some(m) = skipped_message(s.doc().map_err(|e| e.to_string())?, i) {
+    let d = s.doc().map_err(|e| e.to_string())?;
+    for m in read_note_messages(d, i).into_iter().chain(skipped_message(d, i)) {
         eprintln!("{m}");
     }
     if cadcraft_io::is_image(o)
@@ -328,6 +329,12 @@ fn skipped_message(d: &cadcraft_engine::doc::Drawing, path: &str) -> Option<Stri
     Some(format!("{path}: {n} {records} could not be read and are left out ({})", list.join(", ")))
 }
 
+/// The reader's notes on what it left out of `path` (a damaged DWG's unreadable object records,
+/// #859), one warning line each; empty for a complete read.
+fn read_note_messages(d: &cadcraft_engine::doc::Drawing, path: &str) -> Vec<String> {
+    d.read_notes.iter().map(|note| format!("{path}: {note}")).collect()
+}
+
 /// A `--cmd` parameter value of exactly `{last}` is filled in with the last object handle an
 /// earlier `--cmd` in the same run returned: `mview` answers `{"viewports":["100"]}`, so
 /// `viewport.set {"handle":"{last}","scale":1}` changes viewport 100. Any other string is passed
@@ -1091,6 +1098,18 @@ mod tests {
         values.iter().map(|value| (*value).into()).collect()
     }
 
+    #[test]
+    fn read_notes_become_one_warning_each() {
+        let mut d = cadcraft_engine::doc::Drawing::new_imperial();
+        assert!(read_note_messages(&d, "a.dwg").is_empty());
+        d.read_notes =
+            vec!["DWG: 3 of 10 object records couldn't be read and were left out (LINE × 3); saving over the original file would lose them".into()];
+        assert_eq!(
+            read_note_messages(&d, "a.dwg"),
+            vec!["a.dwg: DWG: 3 of 10 object records couldn't be read and were left out (LINE × 3); saving over the original file would lose them"]
+        );
+    }
+
     #[test]
     fn validators_accept_only_the_documented_options() {
         assert!(validate_options("info", &args(&["file.dxf"]), NO_OPTIONS).is_ok());
diff --git a/apps/cadcraft-cli/tests/read_notes.rs b/apps/cadcraft-cli/tests/read_notes.rs
new file mode 100644
--- /dev/null
+++ b/apps/cadcraft-cli/tests/read_notes.rs
@@ -0,0 +1,69 @@
+//! A damaged DWG opens partially (#859); `info` lists the reader's notes on what it left out
+//! (`readNotes`) and `convert` warns with them before writing the copy, so a script or agent sees
+//! the loss. The exit code stays 0.
+
+use std::process::{Command, Output};
+
+fn cli(args: &[&str]) -> Output {
+    Command::new(env!("CARGO_BIN_EXE_cadcraft-cli")).args(args).output().unwrap()
+}
+
+/// A DWG of 200 lines, and a copy damaged so that the reader skips some of its records (found the
+/// way the reader's own test finds one: flipping a few bytes at a time across the objects area).
+fn dwg_and_damaged_copy() -> (Vec<u8>, Vec<u8>) {
+    let mut dxf = String::from("0\nSECTION\n2\nENTITIES\n");
+    for i in 0..200 {
+        dxf += &format!("0\nLINE\n8\n0\n10\n{i}\n20\n0\n11\n{i}\n21\n10\n");
+    }
+    dxf += "0\nENDSEC\n0\nEOF\n";
+    let d = cadcraft_io::read(dxf.as_bytes(), "lines.dxf").unwrap();
+    let opts = cadcraft_io::SaveOptions { version: Some(cadcraft_io::Version::R2000), binary: false };
+    let dwg = cadcraft_io::write_with(&d, "lines.dwg", &opts).unwrap();
+    let damaged = (dwg.len() / 4..dwg.len() * 3 / 5)
+        .step_by(97)
+        .map(|at| {
+            let mut b = dwg.clone();
+            b[at..at + 6].iter_mut().for_each(|x| *x ^= 0xA5);
+            b
+        })
+        .find(|b| cadcraft_io::read(b, "damaged.dwg").is_ok_and(|d| !d.read_notes.is_empty()))
+        .expect("no damaged copy read partially");
+    (dwg, damaged)
+}
+
+#[test]
+fn info_lists_and_convert_warns_about_read_notes() {
+    let dir = std::env::temp_dir().join(format!("cc-cli-read-notes-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let (whole, damaged) = dwg_and_damaged_copy();
+    let path = dir.join("damaged.dwg");
+    std::fs::write(&path, &damaged).unwrap();
+    let path = path.to_str().unwrap();
+
+    let out = cli(&["info", path]);
+    assert!(out.status.success(), "{:?}: {}", out.status, String::from_utf8_lossy(&out.stderr));
+    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
+    let notes = v["readNotes"].as_array().unwrap();
+    assert_eq!(notes.len(), 1, "{v}");
+    assert!(notes[0].as_str().unwrap().contains(" couldn't be read and "), "{v}");
+
+    let copy = dir.join("copy.dxf");
+    let out = cli(&["convert", path, copy.to_str().unwrap()]);
+    assert!(out.status.success(), "{:?}: {}", out.status, String::from_utf8_lossy(&out.stderr));
+    let err = String::from_utf8_lossy(&out.stderr);
+    let warnings: Vec<&str> = err.lines().filter(|l| l.contains(" couldn't be read and ")).collect();
+    assert_eq!(warnings.len(), 1, "{err}");
+    assert!(warnings[0].starts_with(&format!("{path}: DWG: ")), "{err}");
+    assert!(copy.exists());
+
+    // A complete read: no notes, and no warning.
+    let intact = dir.join("whole.dwg");
+    std::fs::write(&intact, &whole).unwrap();
+    let out = cli(&["info", intact.to_str().unwrap()]);
+    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
+    assert_eq!(v["readNotes"], serde_json::json!([]));
+    let out = cli(&["convert", intact.to_str().unwrap(), copy.to_str().unwrap()]);
+    assert!(out.status.success());
+    assert!(!String::from_utf8_lossy(&out.stderr).contains("couldn't be read"), "{}", String::from_utf8_lossy(&out.stderr));
+    let _ = std::fs::remove_dir_all(&dir);
+}
diff --git a/crates/engine/src/cmd/inquiry.rs b/crates/engine/src/cmd/inquiry.rs
--- a/crates/engine/src/cmd/inquiry.rs
+++ b/crates/engine/src/cmd/inquiry.rs
@@ -574,6 +574,8 @@ fn run_inspect(s: &mut Session, p: &Value) -> Result<Value> {
         "view": { "center": [st.view().center.x, st.view().center.y], "height": st.view().height },
         // Records of the file the drawing came from that the reader could not place.
         "skipped": { "count": d.skipped_records.values().fold(0usize, |a, n| a.saturating_add(*n)), "sections": d.skipped_records },
+        // What reading the file left out, in words (a damaged DWG's unreadable object records).
+        "readNotes": d.read_notes,
     });
     if bool_or(p, "entities", true)
         && let (Some(o), Some(store)) = (out.as_object_mut(), d.space(&space))
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -49,7 +49,7 @@ field. `engine.commands` returns a separate command catalog, not a list of addit
 | `cmdline.state` | | Current prompt, keywords, accepted input kinds, history tail. |
 | `engine.execute` | `{command, params}` | Run any command with JSON parameters. Never opens a dialog. `command` is an alias of this method, with the same parameters and result. |
 | `engine.commands` | | Every command: id, label, menu path, shortcut, aliases, params doc, enabled. |
-| `drawing.inspect` | `{entities?, limit?}` | Drawing summary (counts, extents, layers, styles, blocks, layouts, selection, undo) and entities with handles and geometry. |
+| `drawing.inspect` | `{entities?, limit?}` | Drawing summary (counts, extents, layers, styles, blocks, layouts, selection, undo, and what reading the file left out: `skipped` records by section, `readNotes` in words) and entities with handles and geometry. |
 | `ui.inspect` | | UI state, canvas rect, view, cursor and snap, performance counters (`uiFrames`, `passIndex` included); `closePrompt`: the drawing a "Save changes?" prompt is asking about (null when none); and actual egui keyboard focus (`focus.id`, `focus.label`, `focus.role`). |
 | `ui.elements` | `{label?, role?, focused?}` | Observed toolbar, tab, drafting and layer-palette controls from the latest UI frame. Filters match exactly. Each entry has its real egui `id`, label, role, rectangle `[x,y,width,height]`, enabled/visible/selected state and current focus. |
 | `ui.menu.list` / `ui.menu.invoke` | `{command}` | The menu tree; invoke an item like a click (interactive). |
