diff --git a/pkg/agentic/commands_session.go b/pkg/agentic/commands_session.go index 21e260b..528c159 100644 --- a/pkg/agentic/commands_session.go +++ b/pkg/agentic/commands_session.go @@ -14,6 +14,8 @@ func (s *PrepSubsystem) registerSessionCommands() { c.Command("agentic:session/end", core.Command{Description: "End a stored session with status, summary, and handoff notes", Action: s.cmdSessionEnd}) c.Command("session/complete", core.Command{Description: "Mark a stored session completed with status, summary, and handoff notes", Action: s.cmdSessionEnd}) c.Command("agentic:session/complete", core.Command{Description: "Mark a stored session completed with status, summary, and handoff notes", Action: s.cmdSessionEnd}) + c.Command("session/log", core.Command{Description: "Add a work log entry to a stored session", Action: s.cmdSessionLog}) + c.Command("agentic:session/log", core.Command{Description: "Add a work log entry to a stored session", Action: s.cmdSessionLog}) c.Command("session/resume", core.Command{Description: "Resume a paused or handed-off session from local cache", Action: s.cmdSessionResume}) c.Command("agentic:session/resume", core.Command{Description: "Resume a paused or handed-off session from local cache", Action: s.cmdSessionResume}) c.Command("session/replay", core.Command{Description: "Build replay context for a stored session", Action: s.cmdSessionReplay}) @@ -109,6 +111,48 @@ func (s *PrepSubsystem) cmdSessionEnd(options core.Options) core.Result { return core.Result{Value: output, OK: true} } +// core-agent session log ses-abc123 --message="Checked build" --type=checkpoint +func (s *PrepSubsystem) cmdSessionLog(options core.Options) core.Result { + sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") + message := optionStringValue(options, "message") + entryType := optionStringValue(options, "type") + if entryType == "" { + entryType = "info" + } + if sessionID == "" { + core.Print(nil, "usage: core-agent session log --message=\"Checked build\" [--type=checkpoint] [--data='{\"key\":\"value\"}']") + return core.Result{Value: core.E("agentic.cmdSessionLog", "session_id is required", nil), OK: false} + } + if message == "" { + core.Print(nil, "usage: core-agent session log --message=\"Checked build\" [--type=checkpoint] [--data='{\"key\":\"value\"}']") + return core.Result{Value: core.E("agentic.cmdSessionLog", "message is required", nil), OK: false} + } + + result := s.handleSessionLog(s.commandContext(), core.NewOptions( + core.Option{Key: "session_id", Value: sessionID}, + core.Option{Key: "message", Value: message}, + core.Option{Key: "type", Value: entryType}, + core.Option{Key: "data", Value: optionAnyMapValue(options, "data")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdSessionLog", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(SessionLogOutput) + if !ok { + err := core.E("agentic.cmdSessionLog", "invalid session log output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "session: %s", sessionID) + core.Print(nil, "type: %s", entryType) + core.Print(nil, "logged: %s", output.Logged) + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) cmdSessionResume(options core.Options) core.Result { sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") if sessionID == "" { diff --git a/pkg/agentic/commands_session_test.go b/pkg/agentic/commands_session_test.go index c1a008b..2c8047c 100644 --- a/pkg/agentic/commands_session_test.go +++ b/pkg/agentic/commands_session_test.go @@ -25,6 +25,8 @@ func TestCommandsSession_RegisterSessionCommands_Good(t *testing.T) { assert.Contains(t, c.Commands(), "agentic:session/end") assert.Contains(t, c.Commands(), "session/complete") assert.Contains(t, c.Commands(), "agentic:session/complete") + assert.Contains(t, c.Commands(), "session/log") + assert.Contains(t, c.Commands(), "agentic:session/log") assert.Contains(t, c.Commands(), "session/resume") assert.Contains(t, c.Commands(), "agentic:session/resume") assert.Contains(t, c.Commands(), "session/replay") @@ -161,6 +163,69 @@ func TestCommandsSession_CmdSessionEnd_Ugly_InvalidResponse(t *testing.T) { assert.False(t, result.OK) } +func TestCommandsSession_CmdSessionLog_Good(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + require.NoError(t, writeSessionCache(&Session{ + SessionID: "ses-log", + AgentType: "codex", + Status: "active", + WorkLog: []map[string]any{ + {"type": "checkpoint", "message": "build passed"}, + }, + })) + + result := s.cmdSessionLog(core.NewOptions( + core.Option{Key: "session_id", Value: "ses-log"}, + core.Option{Key: "message", Value: "Checked build"}, + core.Option{Key: "type", Value: "checkpoint"}, + core.Option{Key: "data", Value: map[string]any{"repo": "go-io"}}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(SessionLogOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, "Checked build", output.Logged) + + cached, err := readSessionCache("ses-log") + require.NoError(t, err) + require.NotNil(t, cached) + require.Len(t, cached.WorkLog, 2) + assert.Equal(t, "checkpoint", cached.WorkLog[1]["type"]) + assert.Equal(t, "Checked build", cached.WorkLog[1]["message"]) +} + +func TestCommandsSession_CmdSessionLog_Bad_MissingMessage(t *testing.T) { + s := newTestPrep(t) + + result := s.cmdSessionLog(core.NewOptions(core.Option{Key: "session_id", Value: "ses-log"})) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "message is required") +} + +func TestCommandsSession_CmdSessionLog_Ugly_CorruptedCacheFallsBackToRemoteError(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + require.True(t, fs.EnsureDir(sessionCacheRoot()).OK) + require.True(t, fs.WriteAtomic(sessionCachePath("ses-bad"), "{not-json").OK) + + result := s.cmdSessionLog(core.NewOptions( + core.Option{Key: "session_id", Value: "ses-bad"}, + core.Option{Key: "message", Value: "Checked build"}, + )) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "no platform API key configured") +} + func TestCommandsSession_CmdSessionResume_Good(t *testing.T) { dir := t.TempDir() t.Setenv("CORE_WORKSPACE", dir)