more messaging updates

This commit is contained in:
Jacob Dubin
2026-04-19 09:18:43 -05:00
parent fa3867b131
commit cedf08b422
13 changed files with 6773 additions and 10 deletions

View File

@@ -146,6 +146,11 @@ The current evidence in captures, fixtures, and Node behavior supports three mai
Those are the right primary buckets for now. Additional side channels may still emerge later, especially around proactive traffic, direct skill/service sockets, or future on-device OS changes, but they should be treated as extensions to this model until captures prove otherwise.
Latest stock-OS WOD findings:
- `word-of-the-day/right_word` closeout should not emit a synthetic `match`; otherwise Jetstream promotes it into `globalTurnResult` and Global Service relaunches Nimbus a few seconds later with a `Cloud Skill Response Timeout`.
- Voice `play word of the day` hotphrase launch still enters Global Service first, so a synthetic `LISTEN` result alone is not enough. The next-most-correct transport hint is a direct `SKILL_REDIRECT` event aimed at `@be/word-of-the-day`, alongside the menu-shaped `LISTEN` payload.
## Speech, Animation, And ESML
The current joke flow is only a small foothold into Jibo expressiveness.

View File

@@ -82,6 +82,19 @@ public sealed class ResponsePlanToSocketMessagesMapper
}))
};
if (isWordOfDayLaunch)
{
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildSkillRedirectPayload(
transId,
"@be/word-of-the-day",
outboundIntent,
outboundAsrText,
outboundRules,
entities)),
DelayMs: 75));
}
if (emitSkillActions && speak is not null)
{
messages.Add(new SocketReplyPlan(
@@ -165,12 +178,6 @@ public sealed class ResponsePlanToSocketMessagesMapper
intent = string.Empty,
rules,
entities = new Dictionary<string, object?>()
},
match = new
{
intent = string.Empty,
rule = rules.FirstOrDefault() ?? string.Empty,
score = 0.95
}
}
})),
@@ -522,6 +529,44 @@ public sealed class ResponsePlanToSocketMessagesMapper
};
}
private static object BuildSkillRedirectPayload(
string transId,
string skillId,
string outboundIntent,
string outboundAsrText,
IReadOnlyList<string> outboundRules,
object entities)
{
return new
{
type = "SKILL_REDIRECT",
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
msgID = CreateHubMessageId(),
transID = transId,
data = new
{
match = new
{
skillID = skillId,
onRobot = true,
launch = true
},
asr = new
{
text = outboundAsrText,
confidence = 0.95
},
nlu = new
{
confidence = 0.95,
intent = outboundIntent,
rules = outboundRules,
entities
}
}
};
}
private static string EscapeXml(string value)
{
return value

View File

@@ -469,7 +469,7 @@ public sealed class JiboWebSocketServiceTests
}
[Fact]
public async Task ClientAsr_WordOfDayLaunch_EmitsMenuStyleLoadMenuWithSkillAction()
public async Task ClientAsr_WordOfDayLaunch_EmitsMenuStyleLoadMenuAndRedirect()
{
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
@@ -489,13 +489,19 @@ public sealed class JiboWebSocketServiceTests
Text = """{"type":"CLIENT_ASR","transID":"trans-wod-launch","data":{"text":"Play word of the day."}}"""
});
Assert.Equal(2, replies.Count);
Assert.Equal(3, replies.Count);
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
Assert.Equal("menu", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
Assert.Equal(string.Empty, listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
Assert.Equal("word-of-the-day", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("entities").GetProperty("domain").GetString());
Assert.Equal("@be/word-of-the-day", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("skill").GetString());
Assert.Equal("word-of-the-day/menu", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
Assert.Equal("SKILL_REDIRECT", ReadReplyType(replies[2]));
using var redirectPayload = JsonDocument.Parse(replies[2].Text!);
Assert.Equal("@be/word-of-the-day", redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("skillID").GetString());
Assert.True(redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("onRobot").GetBoolean());
Assert.True(redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("launch").GetBoolean());
var session = _store.FindSessionByToken("hub-wod-launch-token");
Assert.NotNull(session);
@@ -551,9 +557,10 @@ public sealed class JiboWebSocketServiceTests
Binary = new byte[3000]
});
Assert.Equal(2, replies.Count);
Assert.Equal(3, replies.Count);
Assert.Equal("LISTEN", ReadReplyType(replies[0]));
Assert.Equal("EOS", ReadReplyType(replies[1]));
Assert.Equal("SKILL_REDIRECT", ReadReplyType(replies[2]));
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
Assert.Equal("menu", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
@@ -632,6 +639,9 @@ public sealed class JiboWebSocketServiceTests
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.False(listenPayload.RootElement.GetProperty("data").TryGetProperty("match", out _));
}
[Fact]
@@ -682,7 +692,7 @@ public sealed class JiboWebSocketServiceTests
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());
Assert.False(listenPayload.RootElement.GetProperty("data").TryGetProperty("match", out _));
var binaryReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{