import (
"context"
"crypto/sha256"
+ "encoding/hex"
"encoding/json"
"errors"
"fmt"
// 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
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)
*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)
// 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")
}