2026-03-17 05:37:57 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
2026-04-02 08:13:16 +00:00
|
|
|
// service := monitor.New(monitor.MonitorOptions{Interval: 30 * time.Second})
|
2026-03-30 22:30:05 +00:00
|
|
|
// service.RegisterTools(server)
|
2026-03-17 05:37:57 +00:00
|
|
|
package monitor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-30 20:37:23 +00:00
|
|
|
"net/url"
|
2026-03-17 05:37:57 +00:00
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-03-21 11:10:31 +00:00
|
|
|
"dappco.re/go/agent/pkg/agentic"
|
2026-03-24 14:46:59 +00:00
|
|
|
"dappco.re/go/agent/pkg/messages"
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
core "dappco.re/go/core"
|
2026-03-29 20:15:58 +00:00
|
|
|
coremcp "forge.lthn.ai/core/mcp/pkg/mcp"
|
2026-03-17 05:37:57 +00:00
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-30 21:22:54 +00:00
|
|
|
// readResult := fs.Read(core.JoinPath(workspaceRoot, name, "status.json"))
|
2026-03-30 21:11:06 +00:00
|
|
|
// if text, ok := resultString(readResult); ok { _ = core.JSONUnmarshalString(text, &workspaceStatus) }
|
2026-03-22 03:41:07 +00:00
|
|
|
var fs = agentic.LocalFs()
|
|
|
|
|
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
func brainKeyPath(home string) string {
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
return core.JoinPath(home, ".claude", "brain.key")
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 15:52:10 +00:00
|
|
|
func monitorAPIURL() string {
|
|
|
|
|
if u := core.Env("CORE_API_URL"); u != "" {
|
|
|
|
|
return u
|
|
|
|
|
}
|
|
|
|
|
return "https://api.lthn.sh"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func monitorBrainKey() string {
|
|
|
|
|
if k := core.Env("CORE_BRAIN_KEY"); k != "" {
|
|
|
|
|
return k
|
|
|
|
|
}
|
2026-03-30 21:11:06 +00:00
|
|
|
if readResult := fs.Read(brainKeyPath(agentic.HomeDir())); readResult.OK {
|
|
|
|
|
if value, ok := resultString(readResult); ok {
|
2026-03-22 15:52:10 +00:00
|
|
|
return core.Trim(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:11:06 +00:00
|
|
|
func resultString(result core.Result) (string, bool) {
|
|
|
|
|
value, ok := result.Value.(string)
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
if !ok {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
return value, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// service := monitor.New(Options{})
|
|
|
|
|
// service.Start(context.Background())
|
2026-03-17 05:37:57 +00:00
|
|
|
type Subsystem struct {
|
2026-03-30 20:00:57 +00:00
|
|
|
*core.ServiceRuntime[Options]
|
2026-03-17 05:37:57 +00:00
|
|
|
server *mcp.Server
|
|
|
|
|
interval time.Duration
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
seenCompleted map[string]bool
|
|
|
|
|
seenRunning map[string]bool
|
|
|
|
|
completionsSeeded bool
|
|
|
|
|
lastInboxMaxID int
|
|
|
|
|
inboxSeeded bool
|
|
|
|
|
lastSyncTimestamp int64
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
2026-03-17 17:45:04 +00:00
|
|
|
poke chan struct{}
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 06:42:42 +00:00
|
|
|
var _ coremcp.Subsystem = (*Subsystem)(nil)
|
|
|
|
|
|
2026-03-24 14:46:59 +00:00
|
|
|
func (m *Subsystem) handleAgentStarted(ev messages.AgentStarted) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.seenRunning[ev.Workspace] = true
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) handleAgentCompleted(ev messages.AgentCompleted) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.seenCompleted[ev.Workspace] = true
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
m.Poke()
|
|
|
|
|
go m.checkIdleAfterDelay()
|
2026-03-24 14:34:56 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 14:54:32 +00:00
|
|
|
func (m *Subsystem) handleWorkspacePushed(ev messages.WorkspacePushed) {
|
|
|
|
|
if m.ServiceRuntime == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
m.syncWorkspacePush(ev.Repo, ev.Branch, ev.Org)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
// c.ACTION(messages.AgentStarted{Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-5"})
|
|
|
|
|
// c.ACTION(messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-5", Status: "completed"})
|
2026-03-29 23:30:08 +00:00
|
|
|
func (m *Subsystem) HandleIPCEvents(_ *core.Core, msg core.Message) core.Result {
|
|
|
|
|
switch ev := msg.(type) {
|
|
|
|
|
case messages.AgentCompleted:
|
|
|
|
|
m.handleAgentCompleted(ev)
|
|
|
|
|
case messages.AgentStarted:
|
|
|
|
|
m.handleAgentStarted(ev)
|
2026-03-31 14:54:32 +00:00
|
|
|
case messages.WorkspacePushed:
|
|
|
|
|
m.handleWorkspacePushed(ev)
|
2026-03-29 23:30:08 +00:00
|
|
|
}
|
|
|
|
|
return core.Result{OK: true}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:13:16 +00:00
|
|
|
// options := monitor.MonitorOptions{Interval: 30 * time.Second}
|
2026-03-30 20:53:36 +00:00
|
|
|
// service := monitor.New(options)
|
2026-04-02 08:13:16 +00:00
|
|
|
type MonitorOptions struct {
|
2026-03-17 05:37:57 +00:00
|
|
|
Interval time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:13:16 +00:00
|
|
|
// Options is kept as a compatibility alias for older callers.
|
|
|
|
|
type Options = MonitorOptions
|
|
|
|
|
|
|
|
|
|
// service := monitor.New(monitor.MonitorOptions{Interval: 30 * time.Second})
|
|
|
|
|
func New(options ...MonitorOptions) *Subsystem {
|
2026-03-17 05:37:57 +00:00
|
|
|
interval := 2 * time.Minute
|
2026-03-30 20:53:36 +00:00
|
|
|
if len(options) > 0 && options[0].Interval > 0 {
|
|
|
|
|
interval = options[0].Interval
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
if envInterval := core.Env("MONITOR_INTERVAL"); envInterval != "" {
|
2026-03-21 19:17:53 +00:00
|
|
|
if d, err := time.ParseDuration(envInterval); err == nil {
|
|
|
|
|
interval = d
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
return &Subsystem{
|
2026-03-21 15:31:16 +00:00
|
|
|
interval: interval,
|
|
|
|
|
poke: make(chan struct{}, 1),
|
|
|
|
|
seenCompleted: make(map[string]bool),
|
2026-03-22 15:14:14 +00:00
|
|
|
seenRunning: make(map[string]bool),
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
func (m *Subsystem) debug(msg string) {
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
core.Debug(msg)
|
2026-03-21 19:17:53 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// name := service.Name() // "monitor"
|
2026-03-17 05:37:57 +00:00
|
|
|
func (m *Subsystem) Name() string { return "monitor" }
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// service.RegisterTools(server)
|
2026-03-17 05:37:57 +00:00
|
|
|
func (m *Subsystem) RegisterTools(server *mcp.Server) {
|
|
|
|
|
m.server = server
|
|
|
|
|
|
|
|
|
|
server.AddResource(&mcp.Resource{
|
|
|
|
|
Name: "Agent Status",
|
|
|
|
|
URI: "status://agents",
|
|
|
|
|
Description: "Current status of all agent workspaces",
|
|
|
|
|
MIMEType: "application/json",
|
|
|
|
|
}, m.agentStatusResource)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// service.Start(ctx)
|
2026-03-17 05:37:57 +00:00
|
|
|
func (m *Subsystem) Start(ctx context.Context) {
|
2026-03-30 21:41:45 +00:00
|
|
|
loopContext, cancel := context.WithCancel(ctx)
|
2026-03-17 05:37:57 +00:00
|
|
|
m.cancel = cancel
|
|
|
|
|
|
2026-03-29 20:15:58 +00:00
|
|
|
core.Info("monitor: started (interval=%s)", m.interval)
|
2026-03-21 19:17:53 +00:00
|
|
|
|
2026-03-17 05:37:57 +00:00
|
|
|
m.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer m.wg.Done()
|
2026-03-30 21:41:45 +00:00
|
|
|
m.loop(loopContext)
|
2026-03-17 05:37:57 +00:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:11:06 +00:00
|
|
|
// result := service.OnStartup(context.Background())
|
|
|
|
|
// core.Println(result.OK)
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
func (m *Subsystem) OnStartup(ctx context.Context) core.Result {
|
2026-03-24 17:42:16 +00:00
|
|
|
m.Start(ctx)
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
return core.Result{OK: true}
|
2026-03-24 17:42:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:11:06 +00:00
|
|
|
// result := service.OnShutdown(context.Background())
|
|
|
|
|
// core.Println(result.OK)
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
func (m *Subsystem) OnShutdown(ctx context.Context) core.Result {
|
|
|
|
|
_ = m.Shutdown(ctx)
|
|
|
|
|
return core.Result{OK: true}
|
2026-03-24 17:42:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// _ = service.Shutdown(ctx)
|
2026-03-17 05:37:57 +00:00
|
|
|
func (m *Subsystem) Shutdown(_ context.Context) error {
|
|
|
|
|
if m.cancel != nil {
|
|
|
|
|
m.cancel()
|
|
|
|
|
}
|
|
|
|
|
m.wg.Wait()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:53:36 +00:00
|
|
|
// service.Poke()
|
2026-03-17 17:45:04 +00:00
|
|
|
func (m *Subsystem) Poke() {
|
|
|
|
|
select {
|
|
|
|
|
case m.poke <- struct{}{}:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 16:08:08 +00:00
|
|
|
func (m *Subsystem) checkIdleAfterDelay() {
|
2026-03-30 20:45:23 +00:00
|
|
|
time.Sleep(5 * time.Second)
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
if m.ServiceRuntime == nil {
|
2026-03-23 16:08:08 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 13:02:41 +00:00
|
|
|
running, queued := m.countLiveWorkspaces()
|
|
|
|
|
if running == 0 && queued == 0 {
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
m.Core().ACTION(messages.QueueDrained{Completed: 0})
|
2026-03-24 13:02:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) countLiveWorkspaces() (running, queued int) {
|
2026-03-30 16:01:32 +00:00
|
|
|
var runtime *core.Core
|
|
|
|
|
if m.ServiceRuntime != nil {
|
|
|
|
|
runtime = m.Core()
|
|
|
|
|
}
|
2026-03-29 21:19:37 +00:00
|
|
|
for _, path := range agentic.WorkspaceStatusPaths() {
|
2026-03-30 21:17:33 +00:00
|
|
|
workspaceDir := core.PathDir(path)
|
|
|
|
|
statusResult := agentic.ReadStatusResult(workspaceDir)
|
2026-03-30 20:45:23 +00:00
|
|
|
if !statusResult.OK {
|
2026-03-30 07:30:42 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
workspaceStatus, ok := statusResult.Value.(*agentic.WorkspaceStatus)
|
|
|
|
|
if !ok || workspaceStatus == nil {
|
2026-03-23 16:08:08 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
switch workspaceStatus.Status {
|
2026-03-24 13:02:41 +00:00
|
|
|
case "running":
|
2026-03-30 20:45:23 +00:00
|
|
|
if workspaceStatus.PID > 0 && processAlive(runtime, workspaceStatus.ProcessID, workspaceStatus.PID) {
|
2026-03-24 13:02:41 +00:00
|
|
|
running++
|
|
|
|
|
}
|
|
|
|
|
case "queued":
|
2026-03-23 16:08:08 +00:00
|
|
|
queued++
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-24 13:02:41 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-03-23 16:08:08 +00:00
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
func processAlive(coreApp *core.Core, processID string, pid int) bool {
|
|
|
|
|
return agentic.ProcessAlive(coreApp, processID, pid)
|
2026-03-22 16:19:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 05:37:57 +00:00
|
|
|
func (m *Subsystem) loop(ctx context.Context) {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 17:45:04 +00:00
|
|
|
m.initSyncTimestamp()
|
|
|
|
|
|
2026-03-17 05:37:57 +00:00
|
|
|
m.check(ctx)
|
|
|
|
|
|
|
|
|
|
ticker := time.NewTicker(m.interval)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
m.check(ctx)
|
2026-03-17 17:45:04 +00:00
|
|
|
case <-m.poke:
|
|
|
|
|
m.check(ctx)
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) check(ctx context.Context) {
|
2026-03-30 21:17:33 +00:00
|
|
|
var statusMessages []string
|
2026-03-17 05:37:57 +00:00
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if statusMessage := m.checkCompletions(); statusMessage != "" {
|
|
|
|
|
statusMessages = append(statusMessages, statusMessage)
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if statusMessage := m.harvestCompleted(); statusMessage != "" {
|
|
|
|
|
statusMessages = append(statusMessages, statusMessage)
|
2026-03-21 12:56:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if statusMessage := m.checkInbox(); statusMessage != "" {
|
|
|
|
|
statusMessages = append(statusMessages, statusMessage)
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if statusMessage := m.syncRepos(); statusMessage != "" {
|
|
|
|
|
statusMessages = append(statusMessages, statusMessage)
|
2026-03-17 17:45:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if len(statusMessages) == 0 {
|
2026-03-17 05:37:57 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
combinedMessage := core.Join("\n", statusMessages...)
|
|
|
|
|
m.notify(ctx, combinedMessage)
|
2026-03-17 05:52:12 +00:00
|
|
|
|
|
|
|
|
if m.server != nil {
|
|
|
|
|
m.server.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{
|
|
|
|
|
URI: "status://agents",
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) checkCompletions() string {
|
2026-03-29 21:11:46 +00:00
|
|
|
entries := agentic.WorkspaceStatusPaths()
|
2026-03-17 05:37:57 +00:00
|
|
|
|
|
|
|
|
running := 0
|
|
|
|
|
queued := 0
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
completed := 0
|
2026-03-21 15:31:16 +00:00
|
|
|
var newlyCompleted []string
|
2026-03-17 05:37:57 +00:00
|
|
|
|
2026-03-21 15:31:16 +00:00
|
|
|
m.mu.Lock()
|
2026-03-21 19:38:03 +00:00
|
|
|
seeded := m.completionsSeeded
|
2026-03-17 05:37:57 +00:00
|
|
|
for _, entry := range entries {
|
2026-03-30 20:45:23 +00:00
|
|
|
entryResult := fs.Read(entry)
|
|
|
|
|
if !entryResult.OK {
|
2026-03-17 05:37:57 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
entryData, ok := resultString(entryResult)
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
var workspaceStatus struct {
|
2026-03-17 05:37:57 +00:00
|
|
|
Status string `json:"status"`
|
|
|
|
|
Repo string `json:"repo"`
|
|
|
|
|
Agent string `json:"agent"`
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
if parseResult := core.JSONUnmarshalString(entryData, &workspaceStatus); !parseResult.OK {
|
2026-03-17 05:37:57 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:30:49 +00:00
|
|
|
workspaceName := agentic.WorkspaceName(core.PathDir(entry))
|
2026-03-21 15:31:16 +00:00
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
switch workspaceStatus.Status {
|
2026-03-17 05:37:57 +00:00
|
|
|
case "completed":
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
completed++
|
2026-03-30 21:30:49 +00:00
|
|
|
if !m.seenCompleted[workspaceName] {
|
|
|
|
|
m.seenCompleted[workspaceName] = true
|
2026-03-21 19:38:03 +00:00
|
|
|
if seeded {
|
2026-03-30 20:45:23 +00:00
|
|
|
newlyCompleted = append(newlyCompleted, core.Sprintf("%s (%s)", workspaceStatus.Repo, workspaceStatus.Agent))
|
2026-03-21 19:38:03 +00:00
|
|
|
}
|
2026-03-21 15:31:16 +00:00
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
case "running":
|
|
|
|
|
running++
|
2026-03-30 21:30:49 +00:00
|
|
|
if !m.seenRunning[workspaceName] && seeded {
|
|
|
|
|
m.seenRunning[workspaceName] = true
|
2026-03-22 15:14:14 +00:00
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
case "queued":
|
|
|
|
|
queued++
|
2026-03-21 15:31:16 +00:00
|
|
|
case "blocked", "failed":
|
2026-03-30 21:30:49 +00:00
|
|
|
if !m.seenCompleted[workspaceName] {
|
|
|
|
|
m.seenCompleted[workspaceName] = true
|
2026-03-21 19:38:03 +00:00
|
|
|
if seeded {
|
2026-03-30 20:45:23 +00:00
|
|
|
newlyCompleted = append(newlyCompleted, core.Sprintf("%s (%s) [%s]", workspaceStatus.Repo, workspaceStatus.Agent, workspaceStatus.Status))
|
2026-03-21 19:38:03 +00:00
|
|
|
}
|
2026-03-21 15:31:16 +00:00
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-21 19:38:03 +00:00
|
|
|
m.completionsSeeded = true
|
2026-03-17 05:37:57 +00:00
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2026-03-21 15:31:16 +00:00
|
|
|
if len(newlyCompleted) == 0 {
|
2026-03-17 05:37:57 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 13:02:41 +00:00
|
|
|
liveRunning, liveQueued := m.countLiveWorkspaces()
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
if m.ServiceRuntime != nil && liveRunning == 0 && liveQueued == 0 {
|
|
|
|
|
m.Core().ACTION(messages.QueueDrained{Completed: len(newlyCompleted)})
|
2026-03-21 12:56:24 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
msg := core.Sprintf("%d agent(s) completed", len(newlyCompleted))
|
2026-03-17 05:37:57 +00:00
|
|
|
if running > 0 {
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
msg = core.Concat(msg, core.Sprintf(", %d still running", running))
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
if queued > 0 {
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
msg = core.Concat(msg, core.Sprintf(", %d queued", queued))
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
return msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) checkInbox() string {
|
2026-03-30 21:17:33 +00:00
|
|
|
brainKey := monitorBrainKey()
|
|
|
|
|
if brainKey == "" {
|
2026-03-29 20:15:58 +00:00
|
|
|
return ""
|
2026-03-22 15:10:03 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
baseURL := monitorAPIURL()
|
|
|
|
|
inboxURL := core.Concat(baseURL, "/v1/messages/inbox?agent=", url.QueryEscape(agentic.AgentName()))
|
|
|
|
|
httpResult := agentic.HTTPGet(context.Background(), inboxURL, core.Trim(brainKey), "Bearer")
|
2026-03-30 20:45:23 +00:00
|
|
|
if !httpResult.OK {
|
2026-03-17 19:27:44 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
2026-03-17 05:37:57 +00:00
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
var inboxResponse struct {
|
2026-03-21 19:17:53 +00:00
|
|
|
Data []struct {
|
2026-03-21 19:31:11 +00:00
|
|
|
ID int `json:"id"`
|
2026-03-17 17:45:04 +00:00
|
|
|
Read bool `json:"read"`
|
2026-03-21 16:05:59 +00:00
|
|
|
From string `json:"from"`
|
2026-03-17 17:45:04 +00:00
|
|
|
Subject string `json:"subject"`
|
2026-03-21 20:08:13 +00:00
|
|
|
Content string `json:"content"`
|
2026-03-21 19:17:53 +00:00
|
|
|
} `json:"data"`
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
2026-03-30 21:17:33 +00:00
|
|
|
if parseResult := core.JSONUnmarshalString(httpResult.Value.(string), &inboxResponse); !parseResult.OK {
|
2026-03-30 20:45:23 +00:00
|
|
|
m.debug("checkInbox: failed to decode response")
|
2026-03-17 05:37:57 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 19:31:11 +00:00
|
|
|
maxID := 0
|
2026-03-17 05:37:57 +00:00
|
|
|
unread := 0
|
2026-03-21 20:08:13 +00:00
|
|
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
prevMaxID := m.lastInboxMaxID
|
|
|
|
|
seeded := m.inboxSeeded
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
type inboxMessage struct {
|
2026-03-21 20:08:13 +00:00
|
|
|
ID int `json:"id"`
|
|
|
|
|
From string `json:"from"`
|
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
}
|
2026-03-30 21:17:33 +00:00
|
|
|
var inboxMessages []inboxMessage
|
2026-03-21 20:08:13 +00:00
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
for _, message := range inboxResponse.Data {
|
2026-03-30 20:45:23 +00:00
|
|
|
if message.ID > maxID {
|
|
|
|
|
maxID = message.ID
|
2026-03-21 19:31:11 +00:00
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
if !message.Read {
|
2026-03-17 05:37:57 +00:00
|
|
|
unread++
|
2026-03-21 20:08:13 +00:00
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
if message.ID > prevMaxID {
|
2026-03-30 21:17:33 +00:00
|
|
|
inboxMessages = append(inboxMessages, inboxMessage{
|
2026-03-30 20:45:23 +00:00
|
|
|
ID: message.ID,
|
|
|
|
|
From: message.From,
|
|
|
|
|
Subject: message.Subject,
|
|
|
|
|
Content: message.Content,
|
2026-03-21 20:08:13 +00:00
|
|
|
})
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.mu.Lock()
|
2026-03-21 19:31:11 +00:00
|
|
|
m.lastInboxMaxID = maxID
|
|
|
|
|
m.inboxSeeded = true
|
2026-03-17 05:37:57 +00:00
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2026-03-21 19:31:11 +00:00
|
|
|
if !seeded {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:17:33 +00:00
|
|
|
if maxID <= prevMaxID || len(inboxMessages) == 0 {
|
2026-03-17 05:37:57 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
if m.ServiceRuntime != nil {
|
2026-03-31 07:27:15 +00:00
|
|
|
m.Core().ACTION(messages.InboxMessage{
|
|
|
|
|
New: len(inboxMessages),
|
|
|
|
|
Total: unread,
|
|
|
|
|
})
|
2026-03-17 17:45:04 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
return core.Sprintf("%d unread message(s) in inbox", unread)
|
2026-03-17 05:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Subsystem) notify(ctx context.Context, message string) {
|
|
|
|
|
if m.server == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
for session := range m.server.Sessions() {
|
|
|
|
|
session.Log(ctx, &mcp.LoggingMessageParams{
|
2026-03-17 05:37:57 +00:00
|
|
|
Level: "info",
|
|
|
|
|
Logger: "monitor",
|
|
|
|
|
Data: message,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
func (m *Subsystem) agentStatusResource(_ context.Context, _ *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
2026-03-29 21:11:46 +00:00
|
|
|
entries := agentic.WorkspaceStatusPaths()
|
2026-03-17 05:37:57 +00:00
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
type workspaceInfo struct {
|
2026-03-17 05:37:57 +00:00
|
|
|
Name string `json:"name"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
Repo string `json:"repo"`
|
|
|
|
|
Agent string `json:"agent"`
|
|
|
|
|
PRURL string `json:"pr_url,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
var workspaces []workspaceInfo
|
2026-03-17 05:37:57 +00:00
|
|
|
for _, entry := range entries {
|
2026-03-30 20:45:23 +00:00
|
|
|
entryResult := fs.Read(entry)
|
|
|
|
|
if !entryResult.OK {
|
2026-03-17 05:37:57 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
entryData, ok := resultString(entryResult)
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
var workspaceStatus struct {
|
2026-03-17 05:37:57 +00:00
|
|
|
Status string `json:"status"`
|
|
|
|
|
Repo string `json:"repo"`
|
|
|
|
|
Agent string `json:"agent"`
|
|
|
|
|
PRURL string `json:"pr_url"`
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
if parseResult := core.JSONUnmarshalString(entryData, &workspaceStatus); !parseResult.OK {
|
2026-03-17 05:37:57 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
workspaces = append(workspaces, workspaceInfo{
|
2026-03-29 21:11:46 +00:00
|
|
|
Name: agentic.WorkspaceName(core.PathDir(entry)),
|
2026-03-30 20:45:23 +00:00
|
|
|
Status: workspaceStatus.Status,
|
|
|
|
|
Repo: workspaceStatus.Repo,
|
|
|
|
|
Agent: workspaceStatus.Agent,
|
|
|
|
|
PRURL: workspaceStatus.PRURL,
|
2026-03-17 05:37:57 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &mcp.ReadResourceResult{
|
|
|
|
|
Contents: []*mcp.ResourceContents{
|
|
|
|
|
{
|
|
|
|
|
URI: "status://agents",
|
|
|
|
|
MIMEType: "application/json",
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
Text: core.JSONMarshalString(workspaces),
|
2026-03-17 05:37:57 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|