Finalize inbox artifacts and error protocol

This commit is contained in:
2026-03-19 03:25:06 +08:00
parent c3314cd9cf
commit f315d2330d
22 changed files with 659 additions and 86 deletions
+78
View File
@@ -0,0 +1,78 @@
package inbox
import (
"strings"
"ai-workflow-skill/internal/protocol"
"ai-workflow-skill/internal/store"
"github.com/spf13/cobra"
)
type artifactOptions struct {
paths []string
kinds []string
metadataJSONs []string
}
func addArtifactFlags(cmd *cobra.Command, opts *artifactOptions) {
cmd.Flags().StringArrayVar(&opts.paths, "artifact", nil, "Artifact path to attach; may be repeated")
cmd.Flags().StringArrayVar(&opts.kinds, "artifact-kind", nil, "Artifact kind; one value applies to all, or match artifact count")
cmd.Flags().StringArrayVar(&opts.metadataJSONs, "artifact-metadata-json", nil, "Artifact metadata JSON; one value applies to all, or match artifact count")
}
func resolveArtifacts(opts artifactOptions) ([]store.ArtifactInput, error) {
if len(opts.paths) == 0 {
if len(opts.kinds) > 0 || len(opts.metadataJSONs) > 0 {
return nil, protocol.InvalidInput("artifact-kind and artifact-metadata-json require at least one artifact path", nil)
}
return nil, nil
}
kinds, err := expandArtifactValues(opts.kinds, len(opts.paths), "artifact-kind")
if err != nil {
return nil, err
}
metadataJSONs, err := expandArtifactValues(opts.metadataJSONs, len(opts.paths), "artifact-metadata-json")
if err != nil {
return nil, err
}
artifacts := make([]store.ArtifactInput, 0, len(opts.paths))
for i, path := range opts.paths {
if strings.TrimSpace(path) == "" {
return nil, protocol.InvalidInput("artifact path cannot be empty", nil)
}
artifact := store.ArtifactInput{
Path: path,
Kind: "file",
}
if len(kinds) > 0 {
artifact.Kind = kinds[i]
}
if len(metadataJSONs) > 0 {
artifact.MetadataJSON = metadataJSONs[i]
}
artifacts = append(artifacts, artifact)
}
return artifacts, nil
}
func expandArtifactValues(values []string, target int, flagName string) ([]string, error) {
switch len(values) {
case 0:
return nil, nil
case 1:
out := make([]string, target)
for i := range out {
out[i] = values[0]
}
return out, nil
case target:
return values, nil
default:
return nil, protocol.InvalidInput(flagName+" must be specified once or once per artifact", nil)
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
package inbox
import (
"fmt"
"os"
"ai-workflow-skill/internal/protocol"
)
func resolveBodyValue(body, bodyFile string) (string, error) {
if body != "" && bodyFile != "" {
return "", fmt.Errorf("body and body-file are mutually exclusive")
return "", protocol.InvalidInput("body and body-file are mutually exclusive", nil)
}
if bodyFile == "" {
return body, nil
@@ -15,7 +16,7 @@ func resolveBodyValue(body, bodyFile string) (string, error) {
content, err := os.ReadFile(bodyFile)
if err != nil {
return "", fmt.Errorf("read body file %q: %w", bodyFile, err)
return "", protocol.InvalidInput("failed to read body-file", err)
}
return string(content), nil
}
+14 -7
View File
@@ -11,9 +11,10 @@ import (
)
type cancelOptions struct {
agent string
threadID string
reason string
agent string
threadID string
reason string
artifacts artifactOptions
}
func newCancelCmd(root *rootOptions) *cobra.Command {
@@ -30,7 +31,11 @@ func newCancelCmd(root *rootOptions) *cobra.Command {
agent = root.agent
}
if agent == "" {
return fmt.Errorf("agent is required")
return protocol.InvalidInput("agent is required", nil)
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := db.Open(ctx, root.dbPath)
@@ -41,9 +46,10 @@ func newCancelCmd(root *rootOptions) *cobra.Command {
s := store.NewInboxStore(sqlDB)
thread, message, err := s.CancelThread(ctx, store.CancelInput{
ThreadID: opts.threadID,
Agent: agent,
Reason: opts.reason,
ThreadID: opts.threadID,
Agent: agent,
Reason: opts.reason,
Artifacts: artifacts,
})
if err != nil {
return err
@@ -70,6 +76,7 @@ func newCancelCmd(root *rootOptions) *cobra.Command {
cmd.Flags().StringVar(&opts.agent, "agent", "", "Acting agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.reason, "reason", "", "Cancellation reason")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
+1 -1
View File
@@ -31,7 +31,7 @@ func newClaimCmd(root *rootOptions) *cobra.Command {
agent = root.agent
}
if agent == "" {
return fmt.Errorf("agent is required")
return protocol.InvalidInput("agent is required", nil)
}
sqlDB, err := db.Open(ctx, root.dbPath)
+8 -1
View File
@@ -17,6 +17,7 @@ type completeOptions struct {
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newDoneCmd(root *rootOptions) *cobra.Command {
@@ -41,13 +42,17 @@ func newCompleteCmd(root *rootOptions, mode string) *cobra.Command {
agent = root.agent
}
if agent == "" {
return fmt.Errorf("agent is required")
return protocol.InvalidInput("agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := db.Open(ctx, root.dbPath)
if err != nil {
@@ -63,6 +68,7 @@ func newCompleteCmd(root *rootOptions, mode string) *cobra.Command {
Body: body,
PayloadJSON: opts.payloadJSON,
Failed: mode == "fail",
Artifacts: artifacts,
})
if err != nil {
return err
@@ -92,6 +98,7 @@ func newCompleteCmd(root *rootOptions, mode string) *cobra.Command {
cmd.Flags().StringVar(&opts.body, "body", "", "Completion body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read completion body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("summary")
+113
View File
@@ -0,0 +1,113 @@
package inbox
import (
"errors"
"fmt"
"io"
"strings"
"ai-workflow-skill/internal/protocol"
"ai-workflow-skill/internal/store"
)
func Execute(args []string, stdout, stderr io.Writer) int {
cmd := NewRootCmd()
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {
jsonOutput := hasJSONFlag(args)
renderError(stdout, stderr, jsonOutput, err)
return exitCodeForError(err)
}
return 0
}
func exitCodeForError(err error) int {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.ExitCode
}
switch {
case isUsageError(err):
return 30
case errors.Is(err, store.ErrLeaseConflict):
return 20
case errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return 40
case errors.Is(err, store.ErrInvalidInput), errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return 30
default:
return 50
}
}
func errorCodeForError(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Code
}
switch {
case isUsageError(err):
return "invalid_input"
case errors.Is(err, store.ErrLeaseConflict):
return "lease_conflict"
case errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return "not_found"
case errors.Is(err, store.ErrInvalidInput):
return "invalid_input"
case errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return "invalid_state"
default:
return "internal_error"
}
}
func renderError(stdout, stderr io.Writer, jsonOutput bool, err error) {
message := errorMessage(err)
if jsonOutput {
_ = protocol.WriteJSON(stdout, protocol.Error{
OK: false,
Error: protocol.ErrorPayload{
Code: errorCodeForError(err),
Message: message,
},
})
return
}
_, _ = fmt.Fprintln(stderr, message)
}
func errorMessage(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Message
}
return err.Error()
}
func hasJSONFlag(args []string) bool {
for _, arg := range args {
if arg == "--json" {
return true
}
if strings.HasPrefix(arg, "--json=") {
return !strings.HasSuffix(arg, "=false")
}
}
return false
}
func isUsageError(err error) bool {
message := err.Error()
return strings.HasPrefix(message, "required flag(s)") ||
strings.HasPrefix(message, "unknown flag:") ||
strings.HasPrefix(message, "unknown command ") ||
strings.Contains(message, " accepts ") ||
strings.Contains(message, "invalid argument ")
}
+3
View File
@@ -48,6 +48,9 @@ func newFetchCmd(root *rootOptions) *cobra.Command {
if err != nil {
return err
}
if len(threads) == 0 {
return protocol.NoMatchingWork("no matching work")
}
resp := protocol.Success{
OK: true,
+135 -21
View File
@@ -309,12 +309,12 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
type commandResult struct {
stdout string
stderr string
err error
exit int
}
waitCh := make(chan commandResult, 1)
go func() {
stdout, stderr, err := executeInboxCommand(
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"wait-reply",
@@ -322,7 +322,7 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
"--after-message", blockedMessageID,
"--timeout-seconds", "2",
)
waitCh <- commandResult{stdout: stdout, stderr: stderr, err: err}
waitCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
@@ -346,8 +346,8 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
t.Fatal("wait-reply command did not return")
}
if waitResult.err != nil {
t.Fatalf("wait-reply failed: %v\nstderr:\n%s", waitResult.err, waitResult.stderr)
if waitResult.exit != 0 {
t.Fatalf("wait-reply failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
@@ -392,12 +392,12 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
type commandResult struct {
stdout string
stderr string
err error
exit int
}
watchCh := make(chan commandResult, 1)
go func() {
stdout, stderr, err := executeInboxCommand(
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
@@ -405,7 +405,7 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
"--status", "pending",
"--timeout-seconds", "2",
)
watchCh <- commandResult{stdout: stdout, stderr: stderr, err: err}
watchCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
@@ -420,6 +420,9 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
"--body-file", bodyPath,
"--artifact", bodyPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"task-brief"}`,
"--run", "run_blog_004",
"--task", "T4",
)
@@ -435,8 +438,8 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
t.Fatal("watch command did not return")
}
if watchResult.err != nil {
t.Fatalf("watch failed: %v\nstderr:\n%s", watchResult.err, watchResult.stderr)
if watchResult.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", watchResult.exit, watchResult.stderr, watchResult.stdout)
}
var watchResp map[string]any
@@ -514,29 +517,123 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
if firstMessage["body"] != "Implement the initial admin post editor." {
t.Fatalf("expected body-file content in first message, got %#v", firstMessage["body"])
}
artifacts, ok := firstMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact on first message, got %#v", firstMessage["artifacts"])
}
firstArtifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if firstArtifact["path"] != bodyPath {
t.Fatalf("expected artifact path %q, got %#v", bodyPath, firstArtifact["path"])
}
if firstArtifact["kind"] != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", firstArtifact["kind"])
}
}
func TestInboxJSONErrorsAndExitCodes(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
if _, _, exitCode := executeInboxCommand("--db", dbPath, "--json", "init"); exitCode != 0 {
t.Fatalf("expected init exit code 0, got %d", exitCode)
}
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-z",
"--status", "pending",
)
if exitCode != 10 {
t.Fatalf("expected fetch no-match exit code 10, got %d", exitCode)
}
assertErrorJSON(t, stdout, "no_matching_work")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected claim missing-thread exit code 40, got %d", exitCode)
}
assertErrorJSON(t, stdout, "not_found")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Review cache settings",
"--summary", "Check cache config",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", threadID,
)
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-y",
"--thread", threadID,
)
if exitCode != 20 {
t.Fatalf("expected lease conflict exit code 20, got %d", exitCode)
}
assertErrorJSON(t, stdout, "lease_conflict")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid body flags",
"--body", "inline",
"--body-file", filepath.Join(t.TempDir(), "missing.md"),
)
if exitCode != 30 {
t.Fatalf("expected invalid input exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func runInboxCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, err := executeInboxCommand(args...)
if err != nil {
t.Fatalf("execute inbox command %v: %v\nstderr:\n%s", args, err, stderr)
stdout, stderr, exitCode := executeInboxCommand(args...)
if exitCode != 0 {
t.Fatalf("execute inbox command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeInboxCommand(args ...string) (string, string, error) {
cmd := NewRootCmd()
func executeInboxCommand(args ...string) (string, string, int) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs(args)
err := cmd.Execute()
return stdout.String(), stderr.String(), err
exitCode := Execute(args, &stdout, &stderr)
return stdout.String(), stderr.String(), exitCode
}
func mustDecodeJSON(t *testing.T, raw string, target any) {
@@ -574,3 +671,20 @@ func nestedValue(t *testing.T, value map[string]any, keys ...string) any {
}
return current
}
func assertErrorJSON(t *testing.T, raw string, expectedCode string) {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
if ok, _ := payload["ok"].(bool); ok {
t.Fatalf("expected ok=false error payload, got %#v", payload)
}
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
if code, _ := errorValue["code"].(string); code != expectedCode {
t.Fatalf("expected error code %q, got %#v", expectedCode, errorValue["code"])
}
}
+3
View File
@@ -49,6 +49,9 @@ func newListCmd(root *rootOptions) *cobra.Command {
if err != nil {
return err
}
if len(threads) == 0 {
return protocol.NoMatchingWork("no matching work")
}
resp := protocol.Success{
OK: true,
+1 -1
View File
@@ -30,7 +30,7 @@ func newRenewCmd(root *rootOptions) *cobra.Command {
agent = root.agent
}
if agent == "" {
return fmt.Errorf("agent is required")
return protocol.InvalidInput("agent is required", nil)
}
sqlDB, err := db.Open(ctx, root.dbPath)
+8 -1
View File
@@ -19,6 +19,7 @@ type replyOptions struct {
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newReplyCmd(root *rootOptions) *cobra.Command {
@@ -35,13 +36,17 @@ func newReplyCmd(root *rootOptions) *cobra.Command {
from = root.agent
}
if from == "" {
return fmt.Errorf("from agent is required")
return protocol.InvalidInput("from agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := db.Open(ctx, root.dbPath)
if err != nil {
@@ -58,6 +63,7 @@ func newReplyCmd(root *rootOptions) *cobra.Command {
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Artifacts: artifacts,
})
if err != nil {
return err
@@ -89,6 +95,7 @@ func newReplyCmd(root *rootOptions) *cobra.Command {
cmd.Flags().StringVar(&opts.body, "body", "", "Reply body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read reply body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("to")
+4 -2
View File
@@ -14,8 +14,10 @@ func NewRootCmd() *cobra.Command {
opts := &rootOptions{}
cmd := &cobra.Command{
Use: "inbox",
Short: "Worker-facing durable coordination bus",
Use: "inbox",
Short: "Worker-facing durable coordination bus",
SilenceErrors: true,
SilenceUsage: true,
}
cmd.PersistentFlags().StringVar(&opts.dbPath, "db", ".agents/coord.db", "SQLite database path")
+9 -2
View File
@@ -23,6 +23,7 @@ type sendOptions struct {
bodyFile string
payloadJSON string
priority string
artifacts artifactOptions
}
func newSendCmd(root *rootOptions) *cobra.Command {
@@ -39,16 +40,20 @@ func newSendCmd(root *rootOptions) *cobra.Command {
from = root.agent
}
if from == "" {
return fmt.Errorf("from agent is required")
return protocol.InvalidInput("from agent is required", nil)
}
if opts.threadID == "" && opts.subject == "" {
return fmt.Errorf("subject is required when creating a new thread")
return protocol.InvalidInput("subject is required when creating a new thread", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := db.Open(ctx, root.dbPath)
if err != nil {
@@ -69,6 +74,7 @@ func newSendCmd(root *rootOptions) *cobra.Command {
Body: body,
PayloadJSON: opts.payloadJSON,
Priority: opts.priority,
Artifacts: artifacts,
})
if err != nil {
return err
@@ -104,6 +110,7 @@ func newSendCmd(root *rootOptions) *cobra.Command {
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read message body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
cmd.Flags().StringVar(&opts.priority, "priority", "normal", "Thread priority")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("to")
+5
View File
@@ -55,6 +55,11 @@ func newShowCmd(root *rootOptions) *cobra.Command {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "- %s\t%s\t%s\n", message.MessageID, message.Kind, message.Summary); err != nil {
return err
}
for _, artifact := range message.Artifacts {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), " artifact\t%s\t%s\n", artifact.Kind, artifact.Path); err != nil {
return err
}
}
}
return nil
},
+8 -1
View File
@@ -18,6 +18,7 @@ type updateOptions struct {
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newUpdateCmd(root *rootOptions) *cobra.Command {
@@ -34,13 +35,17 @@ func newUpdateCmd(root *rootOptions) *cobra.Command {
agent = root.agent
}
if agent == "" {
return fmt.Errorf("agent is required")
return protocol.InvalidInput("agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := db.Open(ctx, root.dbPath)
if err != nil {
@@ -56,6 +61,7 @@ func newUpdateCmd(root *rootOptions) *cobra.Command {
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Artifacts: artifacts,
})
if err != nil {
return err
@@ -86,6 +92,7 @@ func newUpdateCmd(root *rootOptions) *cobra.Command {
cmd.Flags().StringVar(&opts.body, "body", "", "Update body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read update body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("status")
+3 -4
View File
@@ -45,6 +45,9 @@ func newWaitReplyCmd(root *rootOptions) *cobra.Command {
if err != nil {
return err
}
if !result.Woke {
return protocol.NoMatchingWork("no matching reply before timeout")
}
data := map[string]any{
"woke": result.Woke,
@@ -63,10 +66,6 @@ func newWaitReplyCmd(root *rootOptions) *cobra.Command {
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if !result.Woke {
_, err = fmt.Fprintln(cmd.OutOrStdout(), "wait-reply timed out")
return err
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "reply received on thread %s at event %d\n", result.Message.ThreadID, result.NextEventID)
return err
+3 -4
View File
@@ -49,6 +49,9 @@ func newWatchCmd(root *rootOptions) *cobra.Command {
if err != nil {
return err
}
if !result.Woke {
return protocol.NoMatchingWork("no matching work before watch timeout")
}
data := map[string]any{
"woke": result.Woke,
@@ -73,10 +76,6 @@ func newWatchCmd(root *rootOptions) *cobra.Command {
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if !result.Woke {
_, err = fmt.Fprintln(cmd.OutOrStdout(), "watch timed out")
return err
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "watch woke on thread %s at event %d\n", result.Thread.ThreadID, result.NextEventID)
return err