diff --git a/meta/classes-global/sstate.bbclass b/meta/classes-global/sstate.bbclass
index ed88dae17202..c7a21310003c 100644
--- a/meta/classes-global/sstate.bbclass
+++ b/meta/classes-global/sstate.bbclass
@@ -206,6 +206,10 @@ def sstate_state_fromvars(d, task = None):
     ss['fixmedir'] = fixmedir
     return ss
 
+def sstate_logrecords_path(d, tid):
+    recordfn, taskname = tid.rsplit(':', 1)
+    return bb.utils.logrecords_path(d, recordfn, taskname)
+
 def sstate_log_strip_paths(d, message):
     import re
 
@@ -239,6 +243,11 @@ def sstate_log_save(d, record):
     levelno, message = record
     return {'levelno': levelno, 'message': sstate_log_strip_paths(d, message)}
 
+def sstate_log_expand(d, logentry):
+    # Archived record (dict) -> raw logstore entry, with path placeholders
+    # expanded back. Inverse of sstate_log_save.
+    return [logentry['levelno'], sstate_log_expand_paths(d, logentry['message'])]
+
 def sstate_log_emit(d, logentry):
     levelno = logentry.get('levelno', 0)
     if levelno < bb.msg.BBLogFormatter.WARNONCE or "message" not in logentry:
@@ -253,24 +262,84 @@ def sstate_log_emit(d, logentry):
     else:
         bb.warn(message)
 
-python sstate_save_task_log () {
+def sstate_replay_logs(d, logdata):
+    """Replay the log records of the tasks this setscene task was handed via
+    BB_SETSCENE_CLAIMED_TASKS, plus its own task.
+
+    Fold each task's records back into the shared logstore (always, even
+    empty, to clear a stale entry from an earlier native run), so an sstate
+    archive created later in this build still bundles them although the
+    tasks were restored rather than run natively. Then emit them for the
+    user.
+    """
     import json
 
-    # Save the messages logged by this task, so they can be replayed when the
-    # task is restored from sstate. The records are written by bitbake right
-    # after the task body, before the postfuncs.
-    mytask = d.getVar('BB_RUNTASK')
-    recordsfn = os.path.join(d.getVar('T'), 'logrecords.%s.json' % mytask)
-    if not os.path.exists(recordsfn):
-        return
-    with open(recordsfn) as f:
-        records = json.load(f)
+    claimed = (d.getVar('BB_SETSCENE_CLAIMED_TASKS') or '').split()
+    filename = d.getVar('BB_FILENAME', False)
+    claimed.append((filename + ":" if filename else "") +
+                   d.getVar('BB_RUNTASK').removesuffix('_setscene'))
+
+    records = []
+    for task in claimed:
+        taskrecords = logdata.get('tasks', {}).get(task, [])
+        logrecordsfn = sstate_logrecords_path(d, task)
+        bb.utils.mkdirhier(os.path.dirname(logrecordsfn))
+        with open(logrecordsfn, 'w') as f:
+            json.dump([sstate_log_expand(d, e)
+                       for e in taskrecords
+                       if 'levelno' in e and 'message' in e], f)
+        records += taskrecords
+    for logentry in records:
+        sstate_log_emit(d, logentry)
+
+sstate_replay_logs[vardepsexclude] = "BB_SETSCENE_CLAIMED_TASKS BB_FILENAME BB_RUNTASK"
 
-    tasks = {mytask: [sstate_log_save(d, record) for record in records]}
+python sstate_save_task_log () {
+    import json
 
-    logfn = os.path.join(d.getVar('SSTATE_BUILDDIR'), "sstate-log.json")
-    with open(logfn, 'w') as f:
-        json.dump({'tasks': tasks}, f)
+    # Also bundle the messages of the non-sstate tasks we depend on, from this
+    # recipe or from others: they are skipped when restored from sstate, and
+    # bitbake picks which setscene task claims them (BB_SETSCENE_CLAIMED_TASKS).
+    taskdepdata = d.getVar('BB_TASKDEPDATA', False) or {}
+    sstatetasks = d.getVar('SSTATETASKS').split()
+    mytask = d.getVar('BB_RUNTASK')
+    # The running task is always an entry in taskdepdata, under its own tid.
+    # Reconstruct that tid from BB_FILENAME and BB_RUNTASK.
+    filename = d.getVar('BB_FILENAME', False)
+    start = filename + ":" + mytask if filename else None
+    if start not in taskdepdata:
+        bb.fatal("sstate: cannot identify task %s in taskdepdata" % start)
+
+    loggedtasks = {start}
+    seen = {start}
+    todo = [start]
+    while todo:
+        for dep in taskdepdata[todo.pop()].deps:
+            t = taskdepdata[dep]
+            if dep in seen or t.taskname in sstatetasks:
+                continue
+            seen.add(dep)
+            todo.append(dep)
+            loggedtasks.add(dep)
+
+    # Every task writes its records right after its body, before its postfuncs, to
+    # the location returned by logrecords_path(), so the files of all dependencies,
+    # including from other recipes, exist here.
+    tasklogs = {}
+    for tid in loggedtasks:
+        recordsfn = sstate_logrecords_path(d, tid)
+        if os.path.exists(recordsfn):
+            with open(recordsfn) as f:
+                tasklogs[tid] = json.load(f)
+
+    tasks = {}
+    for tid, records in tasklogs.items():
+        tasks[tid] = [sstate_log_save(d, record) for record in records]
+
+    if tasks:
+        logfn = os.path.join(d.getVar('SSTATE_BUILDDIR'), "sstate-log.json")
+        with open(logfn, 'w') as f:
+            json.dump({'tasks': tasks}, f)
 }
 
 def sstate_add(ss, source, dest, d):
@@ -480,11 +549,7 @@ def sstate_installpkgdir(ss, d, replay=False):
         logfp = os.path.join(sstateinst, "sstate-log.json")
         if os.path.exists(logfp):
             with open(logfp, 'r') as logfile:
-                logdata = json.load(logfile)
-            records = logdata.get('tasks', {}).get(d.getVar('BB_RUNTASK').removesuffix('_setscene'), [])
-            for logentry in records:
-                sstate_log_emit(d, logentry)
-
+                sstate_replay_logs(d, json.load(logfile))
         bb.utils.remove(logfp)
 
     return True
