Watch
1
0
Fork
You've already forked souveraine
0

shell: land the owned message delegate and the speech fixes

Applied 0009 then 0008 (0008's re-synthesize control calls a function 0009
adds). Casey applied and reloaded; verified against a running shell rather
than from a clean load.

0009 Speech.qml
  stop() could not stop. playProc ran `sh -c "mpv ... || ffplay ..."` — a
  compound command, so sh does not exec-replace itself and SIGTERM killed the
  wrapper while the player kept sounding as an orphan. Reproduced directly.
  That is the back-to-back TTS slam: every stop left audio playing and the
  next speak started a second player over it. Shell dropped; the pid held is
  now the pid making noise. ffplay fallback deleted rather than repaired —
  needing a fallback is what forced the wrapper that broke the kill.
  resynthesize() added, bypassing the cache: the button existed for "that came
  out wrong" and, being keyed on the same text, always replayed the identical
  file. synthesizing/playing split out of one `speaking` boolean.

0008 AiChat.qml
  One line: delegate AiMessage -> AgentMessage. The vendor block named 8 tools;
  the registry holds 19. The 11 it could not see were exactly the interiority
  surface — outfit, nickname, subagent, atmosphere, reach, consult, itinerary,
  todo, schedule, halt, intrusive.

  regenerate and edit dropped deliberately, per Casey: text regeneration is not
  possible (Ai.regenerate() already returned advice) and there is no in-place
  edit (the vendor wrote to a local array the server never sees). delete is
  armed and states that it hides locally only. Do not restore them.

Queue entries removed here, per the directory's own discipline.
This commit is contained in:
Fimeg 2026-08-12 14:50:57 -04:00
commit 93fe30a8e7
4 changed files with 62 additions and 182 deletions

View file

@ -6,6 +6,7 @@ import qs.modules.common.functions
import qs.modules.souveraine.subconscious
import qs.modules.ii.sidebarLeft.aiChat
import qs.modules.souveraine.island
import qs.modules.souveraine.agent
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@ -446,7 +447,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
return message?.visibleToUser ?? true;
})
}
delegate: AiMessage {
delegate: AgentMessage {
required property var modelData
required property int index
messageIndex: index

View file

@ -1,41 +0,0 @@
From f76c911a2f37352b0846b2ec70a96126ac275ee2 Mon Sep 17 00:00:00 2001
From: Souveraine <souveraine@wiuf.net>
Date: Wed, 12 Aug 2026 13:09:27 -0400
Subject: [PATCH] shell: render the agent surface through the owned message
delegate
The sidebar's message list drew through ii-base's AiMessage, so every tool
call was rendered by the vendor snapshot's ToolCallBlock, which knows 8 of
the registry's 19 tools. The 11 it cannot name are exactly the interiority
verbs. TASK-72 step 2.
Requires modules/souveraine/agent/ composed (deploy.sh MANIFEST, already in
tree). One line reverts it.
---
surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
index bba12a1..0d6e04e 100644
--- a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
+++ b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
@@ -6,6 +6,7 @@ import qs.modules.common.functions
import qs.modules.souveraine.subconscious
import qs.modules.ii.sidebarLeft.aiChat
import qs.modules.souveraine.island
+import qs.modules.souveraine.agent
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -446,7 +447,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
return message?.visibleToUser ?? true;
})
}
- delegate: AiMessage {
+ delegate: AgentMessage {
required property var modelData
required property int index
messageIndex: index
--
2.55.0

View file

@ -1,136 +0,0 @@
From bae112271e6a973bd9d178f68f232774751caf0e Mon Sep 17 00:00:00 2001
From: Souveraine <souveraine@wiuf.net>
Date: Wed, 12 Aug 2026 13:53:26 -0400
Subject: [PATCH] shell: make stop actually stop, and re-synthesis actually
re-synthesize
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three defects in the speech path, all of which present to the user as the
same symptom — audio that piles up and cannot be called back.
stop() could not reach the player. playProc ran ["sh", "-c", "mpv ... ||
ffplay ..."], and a compound command means sh does not exec-replace itself:
it forks the player and waits. Setting running = false SIGTERMs sh, sh dies,
and the player keeps sounding as an orphan. Reproduced directly — the wrapper
died and the child survived. Fixed by invoking mpv with no shell at all, so
the pid quickshell holds is the pid making noise. The ffplay fallback is
dropped rather than repaired: its own invocation was wrong, and needing a
fallback is what forced the wrapper that broke the kill.
--keep-open=no --idle=no added because mpv can finish a stream, print
(Paused) and never exit, which renders as speaking forever.
resynthesize() added. speak() opens with a cache check keyed on the text, and
re-synthesis is by definition the same text, so the re-synthesize control was
guaranteed to hit cache and replay the identical broken audio. Invalidating
_readyText before delegating is the fix.
synthesizing/playing split out of speaking. Synthesis of a short line measured
~11s against the service, and the shell showed the same state throughout as it
shows while actually talking — which is why a press feels unacknowledged and
gets pressed again. speaking is left untouched so nothing downstream shifts
meaning.
---
surfaces/quickshell/services/Speech.qml | 64 +++++++++++++++++++++++--
1 file changed, 60 insertions(+), 4 deletions(-)
diff --git a/surfaces/quickshell/services/Speech.qml b/surfaces/quickshell/services/Speech.qml
index 0655a5f..6e07eb8 100644
--- a/surfaces/quickshell/services/Speech.qml
+++ b/surfaces/quickshell/services/Speech.qml
@@ -33,7 +33,22 @@ Singleton {
id: root
readonly property bool enabled: Config.options?.speech?.tts?.enable ?? false
+
+ // `speaking` is the OR that surfaces have always read — kept as-is so
+ // nothing downstream changes meaning under them.
property bool speaking: synthProc.running || playProc.running
+
+ // But one boolean cannot distinguish "waiting on the synthesizer" from
+ // "audio is coming out of the speaker", and those look nothing alike to a
+ // person: synthesis of a short line measured ~11s against the service,
+ // and during all of it the shell showed the same state it shows while
+ // actually talking. That is the whole reason a press feels unacknowledged
+ // and gets pressed again.
+ //
+ // Split, so a surface can show a spinner for one and a level for the
+ // other, and so a stop button can say which thing it is about to stop.
+ readonly property bool synthesizing: synthProc.running
+ readonly property bool playing: playProc.running
property string lastError: ""
property string _pendingText: ""
@@ -131,6 +146,24 @@ Singleton {
retryTimer.running = false;
}
+ // resynthesize — request fresh audio for text we may already have cached.
+ //
+ // speak() opens with a cache check keyed on the text itself, and
+ // re-synthesis is by definition the *same text* — so calling speak() to
+ // "try again" is guaranteed to hit the cache and replay the identical
+ // broken audio. The one control that exists for "that came out wrong"
+ // could not do the only thing it is for.
+ //
+ // Invalidating _readyText before delegating is the whole fix: it forces
+ // speak() down the synthesis path rather than the playback path.
+ function resynthesize(text) {
+ const t = String(text ?? "").trim();
+ if (t.length === 0 || !root.enabled) return;
+ stop();
+ root._readyText = "";
+ root.speak(t);
+ }
+
// The active agent's own voice, if she has one. The shell picks no voice
// of its own: souveraine owns the who→voice mapping, and that mapping is
// per-agent now — [_souveraine].voice_id rides on the public agent list.
@@ -278,15 +311,38 @@ Singleton {
}
// ── playback ─────────────────────────────────────────────────────────
+ //
+ // No `sh -c` wrapper, deliberately. The previous form was
+ //
+ // ["sh", "-c", "mpv ... || ffplay ..."]
+ //
+ // and a compound command means sh does NOT exec-replace itself: it forks
+ // the player as a child and waits. So `playProc.running = false` sends
+ // SIGTERM to *sh*, sh dies, and the player keeps making noise as an
+ // orphan. Reproduced directly: killing the wrapper left the child alive.
+ //
+ // That is why stop() never stopped anything, and why two speak() calls in
+ // a row played over each other instead of replacing one another.
+ //
+ // One player, invoked directly, so the pid quickshell holds is the pid
+ // making sound. The ffplay fallback is dropped rather than fixed: its
+ // invocation was already wrong (raw input needs -i) and a fallback is
+ // exactly what forced the shell wrapper that broke the kill. mpv is
+ // present on both the laptop and the phone; if it is ever missing, the
+ // honest outcome is a named error, not silent audio nobody can stop.
+ //
+ // --keep-open=no --idle=no is not cosmetic. mpv can reach the end of a
+ // stream, print (Paused), and never exit — which, since `speaking` is
+ // derived from playProc.running, renders as speaking forever.
Process {
id: playProc
- command: ["sh", "-c",
- `mpv --no-video --really-quiet '${root._outFile}' 2>/dev/null ` +
- `|| ffplay -nodisp -autoexit -loglevel quiet '${root._outFile}'`]
+ command: ["mpv", "--no-video", "--really-quiet",
+ "--keep-open=no", "--idle=no", root._outFile]
onExited: (exitCode) => {
// Exit 15 = SIGTERM from stop(); not a real failure — suppress.
+ // This now actually reaches mpv rather than a shell wrapper.
if (exitCode !== 0 && exitCode !== 15) {
- root.lastError = `audio playback failed (exit ${exitCode}) — mpv/ffplay present?`;
+ root.lastError = `audio playback failed (exit ${exitCode}) — is mpv installed?`;
console.log("[Speech]", root.lastError);
}
}
--
2.55.0

View file

@ -33,7 +33,22 @@ Singleton {
id: root
readonly property bool enabled: Config.options?.speech?.tts?.enable ?? false
// `speaking` is the OR that surfaces have always read kept as-is so
// nothing downstream changes meaning under them.
property bool speaking: synthProc.running || playProc.running
// But one boolean cannot distinguish "waiting on the synthesizer" from
// "audio is coming out of the speaker", and those look nothing alike to a
// person: synthesis of a short line measured ~11s against the service,
// and during all of it the shell showed the same state it shows while
// actually talking. That is the whole reason a press feels unacknowledged
// and gets pressed again.
//
// Split, so a surface can show a spinner for one and a level for the
// other, and so a stop button can say which thing it is about to stop.
readonly property bool synthesizing: synthProc.running
readonly property bool playing: playProc.running
property string lastError: ""
property string _pendingText: ""
@ -131,6 +146,24 @@ Singleton {
retryTimer.running = false;
}
// resynthesize request fresh audio for text we may already have cached.
//
// speak() opens with a cache check keyed on the text itself, and
// re-synthesis is by definition the *same text* so calling speak() to
// "try again" is guaranteed to hit the cache and replay the identical
// broken audio. The one control that exists for "that came out wrong"
// could not do the only thing it is for.
//
// Invalidating _readyText before delegating is the whole fix: it forces
// speak() down the synthesis path rather than the playback path.
function resynthesize(text) {
const t = String(text ?? "").trim();
if (t.length === 0 || !root.enabled) return;
stop();
root._readyText = "";
root.speak(t);
}
// The active agent's own voice, if she has one. The shell picks no voice
// of its own: souveraine owns the whovoice mapping, and that mapping is
// per-agent now [_souveraine].voice_id rides on the public agent list.
@ -278,15 +311,38 @@ Singleton {
}
// playback
//
// No `sh -c` wrapper, deliberately. The previous form was
//
// ["sh", "-c", "mpv ... || ffplay ..."]
//
// and a compound command means sh does NOT exec-replace itself: it forks
// the player as a child and waits. So `playProc.running = false` sends
// SIGTERM to *sh*, sh dies, and the player keeps making noise as an
// orphan. Reproduced directly: killing the wrapper left the child alive.
//
// That is why stop() never stopped anything, and why two speak() calls in
// a row played over each other instead of replacing one another.
//
// One player, invoked directly, so the pid quickshell holds is the pid
// making sound. The ffplay fallback is dropped rather than fixed: its
// invocation was already wrong (raw input needs -i) and a fallback is
// exactly what forced the shell wrapper that broke the kill. mpv is
// present on both the laptop and the phone; if it is ever missing, the
// honest outcome is a named error, not silent audio nobody can stop.
//
// --keep-open=no --idle=no is not cosmetic. mpv can reach the end of a
// stream, print (Paused), and never exit which, since `speaking` is
// derived from playProc.running, renders as speaking forever.
Process {
id: playProc
command: ["sh", "-c",
`mpv --no-video --really-quiet '${root._outFile}' 2>/dev/null ` +
`|| ffplay -nodisp -autoexit -loglevel quiet '${root._outFile}'`]
command: ["mpv", "--no-video", "--really-quiet",
"--keep-open=no", "--idle=no", root._outFile]
onExited: (exitCode) => {
// Exit 15 = SIGTERM from stop(); not a real failure suppress.
// This now actually reaches mpv rather than a shell wrapper.
if (exitCode !== 0 && exitCode !== 15) {
root.lastError = `audio playback failed (exit ${exitCode}) mpv/ffplay present?`;
root.lastError = `audio playback failed (exit ${exitCode}) is mpv installed?`;
console.log("[Speech]", root.lastError);
}
}