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:
parent
cb3785bcbd
commit
93fe30a8e7
4 changed files with 62 additions and 182 deletions
|
|
@ -6,6 +6,7 @@ import qs.modules.common.functions
|
||||||
import qs.modules.souveraine.subconscious
|
import qs.modules.souveraine.subconscious
|
||||||
import qs.modules.ii.sidebarLeft.aiChat
|
import qs.modules.ii.sidebarLeft.aiChat
|
||||||
import qs.modules.souveraine.island
|
import qs.modules.souveraine.island
|
||||||
|
import qs.modules.souveraine.agent
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
@ -446,7 +447,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||||
return message?.visibleToUser ?? true;
|
return message?.visibleToUser ?? true;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
delegate: AiMessage {
|
delegate: AgentMessage {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
messageIndex: index
|
messageIndex: index
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -33,7 +33,22 @@ Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property bool enabled: Config.options?.speech?.tts?.enable ?? false
|
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
|
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 lastError: ""
|
||||||
|
|
||||||
property string _pendingText: ""
|
property string _pendingText: ""
|
||||||
|
|
@ -131,6 +146,24 @@ Singleton {
|
||||||
retryTimer.running = false;
|
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
|
// 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
|
// 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.
|
// per-agent now — [_souveraine].voice_id rides on the public agent list.
|
||||||
|
|
@ -278,15 +311,38 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── playback ─────────────────────────────────────────────────────────
|
// ── 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 {
|
Process {
|
||||||
id: playProc
|
id: playProc
|
||||||
command: ["sh", "-c",
|
command: ["mpv", "--no-video", "--really-quiet",
|
||||||
`mpv --no-video --really-quiet '${root._outFile}' 2>/dev/null ` +
|
"--keep-open=no", "--idle=no", root._outFile]
|
||||||
`|| ffplay -nodisp -autoexit -loglevel quiet '${root._outFile}'`]
|
|
||||||
onExited: (exitCode) => {
|
onExited: (exitCode) => {
|
||||||
// Exit 15 = SIGTERM from stop(); not a real failure — suppress.
|
// 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) {
|
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);
|
console.log("[Speech]", root.lastError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue