more code matching for skill launch

This commit is contained in:
Jacob Dubin
2026-04-19 09:00:47 -05:00
parent bacaa6f2ca
commit fa3867b131
14 changed files with 6232 additions and 6 deletions

View File

@@ -110,6 +110,7 @@ Evidence from the smaller `2026-04-18/19` hotphrase and word-of-the-day verifica
- the `jibo test 5` bundle suggests the remaining WOD launch and post-win cleanup bugs share the same root cause: we were leaving the robot-side `cloudSkillResponse` promise unresolved on `word_of_the_day`, `word_of_the_day_guess`, and `word-of-the-day/right_word`, so the latest .NET pass now emits a completion-only silent `SKILL_ACTION` for those paths instead of stopping at `LISTEN` + `EOS` or going fully silent
- the `jibo test 6` bundle plus the attached `@be` source snapshot refine that diagnosis: Nimbus does accept the silent completion response, but treats it as a normal `SLIM/RUNTIME_PROMPT` instead of a skill redirect, while the successful on-robot path is built around `menu + domain=word-of-the-day` skill switching through `SkillSwitchScheduler`
- the attached `be-framework.js` adds one more strong clue: the Be relaunch hook reads `skillData.nlu.skill`, so synthetic cloud launch turns for word-of-the-day should carry the explicit target skill name in the outbound NLU payload instead of expecting the robot to infer it from `intent/domain` alone
- the `JiboOs/V3.1` Nimbus source confirms the hotphrase/global launch path still routes through `@be/nimbus` and waits on `listenResult.cloudSkillResponse`, while Nimbus only supports a narrow set of cloud JCP behaviors and does not use cloud `REDIRECT` to jump into local skills; by contrast, the post-win `word-of-the-day/right_word` turn is a local `Optional-Response`, so the cleaner robot-side closeout is to synthesize an immediate empty `LISTEN + EOS` no-response result rather than replying with only `SKILL_ACTION`
- the same `jibo test 6` capture also shows the blue-ring cleanup loop was partly self-inflicted in `.NET`: after `word-of-the-day/right_word` we stopped the active turn, but later stray binary audio on the same transID could still re-arm buffering even without a fresh `LISTEN`, so the next pass now requires a real listen phase before post-turn audio can reopen buffered completion
- the local buffered-audio seam is still producing repeated `whisper.cpp returned no transcript` and `ffmpeg ... Codec not found` failures, so lightweight waveform or energy screening is worth considering once the core launch flow is stable

View File

@@ -143,6 +143,48 @@ public sealed class ResponsePlanToSocketMessagesMapper
];
}
public static IReadOnlyList<SocketReplyPlan> MapNoInput(string transId, IReadOnlyList<string> rules)
{
return
[
new SocketReplyPlan(JsonSerializer.Serialize(new
{
type = "LISTEN",
transID = transId,
data = new
{
asr = new
{
confidence = 0.95,
final = true,
text = string.Empty
},
nlu = new
{
confidence = 0.95,
intent = string.Empty,
rules,
entities = new Dictionary<string, object?>()
},
match = new
{
intent = string.Empty,
rule = rules.FirstOrDefault() ?? string.Empty,
score = 0.95
}
}
})),
new SocketReplyPlan(JsonSerializer.Serialize(new
{
type = "EOS",
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
msgID = CreateHubMessageId(),
transID = transId,
data = new { }
}))
];
}
private static IReadOnlyList<string> ReadRules(TurnContext turn, string? messageType)
{
var attributeName = string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)

View File

@@ -150,9 +150,9 @@ public sealed class WebSocketTurnFinalizationService(
ResetBufferedAudio(session);
session.TurnState.SawListen = false;
session.TurnState.SawContext = false;
return ResponsePlanToSocketMessagesMapper.MapCompletionOnly(
return ResponsePlanToSocketMessagesMapper.MapNoInput(
session.TurnState.TransId ?? session.LastTransId ?? string.Empty,
"@be/word-of-the-day")
session.TurnState.ListenRules)
.Select(map => new WebSocketReply
{
Text = map.Text,
@@ -433,7 +433,15 @@ public sealed class WebSocketTurnFinalizationService(
ResetBufferedAudio(session);
turnState.SawListen = false;
turnState.SawContext = false;
return [];
return ResponsePlanToSocketMessagesMapper.MapNoInput(
turnState.TransId ?? session.LastTransId ?? string.Empty,
turnState.ListenRules)
.Select(map => new WebSocketReply
{
Text = map.Text,
DelayMs = map.DelayMs
})
.ToArray();
}
if (ShouldIgnoreInitialEmptyHotphraseTurn(finalizedTurn, turnState))

View File

@@ -629,7 +629,9 @@ public sealed class JiboWebSocketServiceTests
Text = """{"type":"CLIENT_ASR","transID":"trans-wod-right-word","data":{}}"""
});
Assert.Empty(replies);
Assert.Equal(2, replies.Count);
Assert.Equal("LISTEN", ReadReplyType(replies[0]));
Assert.Equal("EOS", ReadReplyType(replies[1]));
}
[Fact]
@@ -674,8 +676,13 @@ public sealed class JiboWebSocketServiceTests
Text = """{"type":"LISTEN","transID":"trans-wod-right-word-audio","data":{"rules":["word-of-the-day/right_word","globals/gui_nav","globals/mim_repeat","globals/global_commands_launch"]}}"""
});
Assert.Single(replies);
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[0]));
Assert.Equal(2, replies.Count);
Assert.Equal("LISTEN", ReadReplyType(replies[0]));
Assert.Equal("EOS", ReadReplyType(replies[1]));
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
Assert.Equal(string.Empty, listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
Assert.Equal("word-of-the-day/right_word", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
var binaryReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{