]> git.giacomolli.uk - onechat/blobdiff - notify.go
Select notification targets by stable ID
[onechat] / notify.go
index 1036312c176d2983a7f1b5e8d15284f2be8b9089..d4e4e7a68b10e70149d7ea0a775155b23e1ace6a 100644 (file)
--- a/notify.go
+++ b/notify.go
@@ -3,6 +3,7 @@ package main
 import (
        "context"
        "crypto/sha256"
+       "encoding/hex"
        "encoding/json"
        "errors"
        "fmt"
@@ -36,33 +37,40 @@ const (
        // time.
        notifyTimeout = 10 * time.Second
 
-       notifyUsage = `Usage: onechat notify --source SOURCE --id ID --text TEXT
+       notifyUsage = `Usage: onechat notify [--agent-id ID] --source SOURCE --id ID --text TEXT
 
-Send untrusted external data to the root agent at its next turn boundary.
+Send untrusted external data at the selected agent's next turn boundary.
+Omit --agent-id for root. An explicit ID must identify the root or a permanent
+child, not a task worker. Names are not accepted.
 Success means durably accepted, not processed. Retry with the same source,
-ID, and text after an uncertain result. Changed text for an existing key fails.
+ID, target identity, and text after an uncertain result. A changed target or
+text for an existing key fails.
 Text is limited to 32768 bytes. Source and ID are 1 to 128 ASCII letters,
 digits, dots, underscores, or hyphens. The local test interface keeps up to
 1024 receipts without expiry and rejects new keys when full.
-Child targets, receipt expiry, and processing status are not supported.
+Receipt expiry and processing status are not supported.
 `
 )
 
 // NotifyInput is caller-supplied data. Its source label proves no identity or
-// authority. Source and ID together identify a retry of the same text.
+// authority. Source and ID together identify a retry of the same text. AgentID
+// selects a permanent agent. An empty AgentID selects the root.
 type NotifyInput struct {
-       Source string `json:"source"`
-       ID     string `json:"id"`
-       Text   string `json:"text"`
+       AgentID string `json:"agent_id,omitempty"`
+       Source  string `json:"source"`
+       ID      string `json:"id"`
+       Text    string `json:"text"`
 }
 
 // NotifyReceipt keeps a retry key after its queued delivery finishes. Digest
-// binds the original text without retaining another copy of that text.
+// binds the original text without retaining another copy of that text. TargetID
+// binds the permanent agent identity. An empty TargetID selects the root.
 type NotifyReceipt struct {
-       Source string `json:"source"`
-       ID     string `json:"id"`
-       Digest string `json:"digest"`
-       Number int    `json:"number"`
+       TargetID string `json:"target_id,omitempty"`
+       Source   string `json:"source"`
+       ID       string `json:"id"`
+       Digest   string `json:"digest"`
+       Number   int    `json:"number"`
 }
 
 // notify submits a local event and prints only its acceptance receipt. The
@@ -166,9 +174,10 @@ func notifyParse(args []string) (NotifyInput, error) {
        var input NotifyInput
 
        fields := map[string]*string{
-               "--source": &input.Source,
-               "--id":     &input.ID,
-               "--text":   &input.Text,
+               "--agent-id": &input.AgentID,
+               "--source":   &input.Source,
+               "--id":       &input.ID,
+               "--text":     &input.Text,
        }
 
        seen := make(map[string]bool)
@@ -192,6 +201,10 @@ func notifyParse(args []string) (NotifyInput, error) {
                *field = value
        }
 
+       if seen["--agent-id"] && input.AgentID == "" {
+               return input, errors.New("notification agent ID must not be empty")
+       }
+
        err := notifyValidate(input)
        if err != nil {
                return input, fmt.Errorf("validate notification: %w", err)
@@ -202,6 +215,13 @@ func notifyParse(args []string) (NotifyInput, error) {
 
 // notifyValidate checks both CLI and direct socket inputs without echoing data.
 func notifyValidate(input NotifyInput) error {
+       if input.AgentID != "" {
+               id, err := hex.DecodeString(input.AgentID)
+               if err != nil || len(id) != agentIDBytes {
+                       return errors.New("notification agent ID is invalid")
+               }
+       }
+
        if !notifyIdentifier(input.Source) || !notifyIdentifier(input.ID) {
                return errors.New("notification source and ID must be 1 to 128 ASCII letters, digits, dots, underscores, or hyphens")
        }