J2: the tool runner tells a subscribed bundle how its module's consumer stands, as mesh/heartbeat (stdio.OnHeartbeat), at each idle beat and once Missed (ADR 0615, ADR 0621, issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:10:25 +01:00
parent d71cd5cef6
commit f87c9720ed
5 changed files with 241 additions and 11 deletions
+29 -2
View File
@@ -775,6 +775,7 @@ type consumers struct {
type moduleEvents struct {
stop func()
delivers []*func(json.RawMessage) error
beats []*func(json.RawMessage)
}
// stopAll unbinds every module's consumer.
@@ -822,6 +823,7 @@ type moduleBus struct {
module string
mu sync.Mutex
deliver *func(json.RawMessage) error
beat *func(json.RawMessage)
}
// Publish emits an event as the module (ADR 0193).
@@ -859,7 +861,7 @@ func (b *moduleBus) Ask(params json.RawMessage) (json.RawMessage, error) {
// Subscribe hands this bundle the module's events. The consumer is bound once per module; each of
// the module's children that subscribed is handed every event, and the event is acknowledged only
// when all of them took it — one consumer split between two readers would give each half.
func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error) error {
func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error, beat func(json.RawMessage)) error {
b.mu.Lock()
if b.deliver == nil {
d := deliver
@@ -868,6 +870,13 @@ func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error) error {
*b.deliver = deliver
}
mine := b.deliver
if b.beat == nil {
x := beat
b.beat = &x
} else {
*b.beat = beat
}
mineBeat := b.beat
b.mu.Unlock()
c := b.all
@@ -886,12 +895,14 @@ func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error) error {
}
if !listed {
m.delivers = append(m.delivers, mine)
m.beats = append(m.beats, mineBeat)
}
if m.stop != nil {
return nil
}
module := b.module
stop, err := c.conn.ConsumeAs(module, func(env bus.Envelope) error { return c.handOut(module, m, env) })
stop, err := c.conn.ConsumeAsBeating(module, func(env bus.Envelope) error { return c.handOut(module, m, env) },
func(b bus.Beat) { c.handBeat(m, b) })
if err != nil {
return err
}
@@ -900,6 +911,22 @@ func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error) error {
return nil
}
// handBeat hands the word on a module's consumer to every child of the module that subscribed, as an event is.
func (c *consumers) handBeat(m *moduleEvents, b bus.Beat) {
raw, err := json.Marshal(b)
if err != nil {
return
}
c.mu.Lock()
targets := append([](*func(json.RawMessage))(nil), m.beats...)
c.mu.Unlock()
for _, t := range targets {
if f := *t; f != nil {
f(raw)
}
}
}
// errModuleReloading refuses an event heard by a module's consumer that was stopped for a reload.
var errModuleReloading = errors.New("the module's bundle is being loaded again; offered again")