feat: filter receive messages by since

This commit is contained in:
zhaoyilun
2026-07-10 01:27:27 +08:00
parent e7324d8aa6
commit aa636f5a33
9 changed files with 164 additions and 14 deletions

View File

@@ -26,7 +26,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract. - C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract.
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke. - `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke.
- `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility. - `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility.
- `isphere_receive_messages` accepts `conversation_id`, `query`, and `limit` for local readonly PacketReader evidence filtering. - `isphere_receive_messages` accepts `conversation_id`, `query`, `since`, and `limit` for local readonly PacketReader evidence filtering.
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures. - N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only. - N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
@@ -53,7 +53,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop ## Current loop
Current loop: `Stage C / Loop C19 - receive-message since time filter slice`. Current loop: `Stage C / Loop C20 - receive-message remaining contract argument validation`.
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
@@ -84,10 +84,11 @@ Current loop output so far:
16. C16: configured PacketReader source hardening added `ISPHERE_PACKET_LOG_DIR`, deterministic recursive `.log`/`.txt` directory loading, ambiguous file/dir config failure, and configured-directory receive/contact/group/file smoke verification. 16. C16: configured PacketReader source hardening added `ISPHERE_PACKET_LOG_DIR`, deterministic recursive `.log`/`.txt` directory loading, ambiguous file/dir config failure, and configured-directory receive/contact/group/file smoke verification.
17. C17: `isphere_receive_messages` output was aligned to the core read contract envelope and message fields while retaining legacy aliases; verify-go-mcp now asserts contract fields for configured file and directory source smokes. 17. C17: `isphere_receive_messages` output was aligned to the core read contract envelope and message fields while retaining legacy aliases; verify-go-mcp now asserts contract fields for configured file and directory source smokes.
18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification. 18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification.
19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling.
## Next business mainline ## Next business mainline
1. Run Stage C Loop C19: add the next safe `isphere_receive_messages` filter (`since`) so digital employees can target recent configured local-readonly evidence by time. 1. Run Stage C Loop C20: accept and validate the remaining safe `isphere_receive_messages` contract arguments (`include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`) without pretending unsupported connectors/pagination exist.
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated. 2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated. 3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
4. Keep UIA selector/report work as fallback support, not as the primary roadmap. 4. Keep UIA selector/report work as fallback support, not as the primary roadmap.

View File

@@ -184,7 +184,7 @@ Allowed parameters:
- `win_helper_self_check`: zero business parameters. - `win_helper_self_check`: zero business parameters.
- `win_helper_scan_windows`: `include_all_visible` only. - `win_helper_scan_windows`: `include_all_visible` only.
- `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only. - `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only.
- `isphere_receive_messages`: `conversation_id`, `query`, `limit` only. - `isphere_receive_messages`: `conversation_id`, `query`, `since`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied.
- `isphere_search_contacts`: `query`, `limit` only. - `isphere_search_contacts`: `query`, `limit` only.
- `isphere_search_groups`: `query`, `limit` only. - `isphere_search_groups`: `query`, `limit` only.
- `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred. - `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred.
@@ -202,7 +202,7 @@ The current runbook verifies eight tools:
- `isphere_search_groups` - `isphere_search_groups`
- `isphere_receive_files` - `isphere_receive_files`
The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, and supports `conversation_id` plus keyword `query` filtering. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred. The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, and supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred.
Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place. Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place.

View File

@@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice | | MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C18 supports contract-shaped output plus `conversation_id`, `query`, and `limit` filters; default source remains empty when unset | add `since` time filter | | `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C19 supports contract-shaped output plus `conversation_id`, `query`, `since`, and `limit` filters; default source remains empty when unset | validate remaining safe contract args |
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence | | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence |
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence |
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output |

View File

@@ -1634,19 +1634,19 @@ Implemented and verified in this loop:
- Keep `cursor`, `source_preference`, and broader pagination deferred. - Keep `cursor`, `source_preference`, and broader pagination deferred.
- Do not add write/send/file-download behavior. - Do not add write/send/file-download behavior.
- [ ] **Step 1: Write failing source `since` test** - [x] **Step 1: Write failing source `since` test**
Use synthetic PacketReader fixtures before and after an RFC3339 threshold. Use synthetic PacketReader fixtures before and after an RFC3339 threshold.
- [ ] **Step 2: Implement source `since` filtering** - [x] **Step 2: Implement source `since` filtering**
Parse `since` once per receive call and filter normalized messages by timestamp. Parse `since` once per receive call and filter normalized messages by timestamp.
- [ ] **Step 3: Wire MCP args and verification** - [x] **Step 3: Wire MCP args and verification**
Expose `since` on `isphere_receive_messages`, update tool tests and `verify-go-mcp.ps1` synthetic smoke. Expose `since` on `isphere_receive_messages`, update tool tests and `verify-go-mcp.ps1` synthetic smoke.
- [ ] **Step 4: Run full verification and commit** - [x] **Step 4: Run full verification and commit**
Run: Run:
@@ -1661,6 +1661,65 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
--- ---
## Loop C19 Result
Implemented and verified in this loop:
- `isphere.ReceiveMessagesQuery` now supports `Since` in addition to `ConversationID`, `Query`, and `Limit`.
- `EncryptedPacketLogSource.ReceiveMessages` accepts RFC3339/RFC3339Nano `since` input, compares it against PacketReader epoch-millisecond message timestamps, and returns only messages at or after the threshold.
- Invalid `since` input now returns an explicit error instead of being silently ignored.
- `isphere_receive_messages` MCP args now accept `since` and pass it through to the source.
- `scripts/verify-go-mcp.ps1` now calls configured file and directory source smokes with `conversation_id`, `query`, and `since`.
---
## Loop C20: Receive-message remaining contract argument validation
**Goal:** Make `isphere_receive_messages` friendlier to contract-following digital employees by accepting the remaining safe read-contract parameters with explicit validation, while keeping unsupported pagination/source behavior honest.
**Planned files:**
- Modify: `internal/tools/isphere_read.go`
- Modify: `internal/tools/isphere_read_test.go`
- Modify: `scripts/verify-go-mcp.ps1`
- Modify: `docs/go-mcp-runbook.md`
- Modify: `docs/current-status-card.md`
- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`
**Planned behavior:**
- Add MCP args for `include_attachment_metadata`, `source_preference`, `preview`, and `cursor`.
- Accept `source_preference` empty, `auto`, or `local_readonly`; reject bridge/uia/network until those connectors exist.
- Accept empty `cursor`; reject non-empty cursor until pagination is implemented.
- Treat `preview` as read-only/no mutation and keep the audit `execution_mode` as `read`.
- Treat `include_attachment_metadata=false` as a request to suppress inline receive-message attachments while leaving `isphere_receive_files` available for explicit file listing.
- Do not add write/send/file-download behavior.
- [ ] **Step 1: Write failing MCP argument validation tests**
Extend `internal/tools/isphere_read_test.go` with accepted safe args and rejected unsupported source/cursor cases.
- [ ] **Step 2: Implement argument validation and attachment toggle**
Add minimal validation/helpers in `internal/tools/isphere_read.go`.
- [ ] **Step 3: Update verification and docs**
Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan.
- [ ] **Step 4: Run full verification and commit**
Run:
```powershell
git diff --check
go test ./...
go build ./cmd/isphere-mcp
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
```
Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C20 paths only and rewrite the next loop from actual results.
---
## Self-Review ## Self-Review
- Spec coverage: the plan follows the user's cyclic requirement: plan first, implement one loop at a time, update the next loop after each result, and ask only on blockers. - Spec coverage: the plan follows the user's cyclic requirement: plan first, implement one loop at a time, update the next loop after each result, and ask only on blockers.

View File

@@ -3,7 +3,9 @@ package isphere
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time"
"isphere-ai-bridge/internal/isphere/logcodec" "isphere-ai-bridge/internal/isphere/logcodec"
"isphere-ai-bridge/internal/isphere/packetlog" "isphere-ai-bridge/internal/isphere/packetlog"
@@ -25,6 +27,7 @@ type Message struct {
type ReceiveMessagesQuery struct { type ReceiveMessagesQuery struct {
ConversationID string ConversationID string
Query string Query string
Since string
Limit int Limit int
} }
@@ -37,6 +40,10 @@ type EncryptedPacketLogSource struct {
} }
func (s EncryptedPacketLogSource) ReceiveMessages(ctx context.Context, query ReceiveMessagesQuery) (ReceiveMessagesResult, error) { func (s EncryptedPacketLogSource) ReceiveMessages(ctx context.Context, query ReceiveMessagesQuery) (ReceiveMessagesResult, error) {
since, hasSince, err := parseReceiveMessagesSince(query.Since)
if err != nil {
return ReceiveMessagesResult{}, err
}
limit := query.Limit limit := query.Limit
if limit <= 0 || limit > len(s.Lines) { if limit <= 0 || limit > len(s.Lines) {
limit = len(s.Lines) limit = len(s.Lines)
@@ -57,7 +64,7 @@ func (s EncryptedPacketLogSource) ReceiveMessages(ctx context.Context, query Rec
return ReceiveMessagesResult{}, fmt.Errorf("parse packet log line %d: %w", index, err) return ReceiveMessagesResult{}, fmt.Errorf("parse packet log line %d: %w", index, err)
} }
message := normalizePacketMessage(parsed) message := normalizePacketMessage(parsed)
if !receiveMessageMatchesQuery(message, query) { if !receiveMessageMatchesQuery(message, query) || !receiveMessageMatchesSince(message, since, hasSince) {
continue continue
} }
messages = append(messages, message) messages = append(messages, message)
@@ -68,6 +75,18 @@ func (s EncryptedPacketLogSource) ReceiveMessages(ctx context.Context, query Rec
return ReceiveMessagesResult{Messages: messages}, nil return ReceiveMessagesResult{Messages: messages}, nil
} }
func parseReceiveMessagesSince(value string) (time.Time, bool, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return time.Time{}, false, nil
}
parsed, err := time.Parse(time.RFC3339Nano, trimmed)
if err != nil {
return time.Time{}, false, fmt.Errorf("parse receive messages since %q: %w", value, err)
}
return parsed.UTC(), true, nil
}
func receiveMessageMatchesQuery(message Message, query ReceiveMessagesQuery) bool { func receiveMessageMatchesQuery(message Message, query ReceiveMessagesQuery) bool {
if strings.TrimSpace(query.ConversationID) != "" && message.ConversationID != strings.TrimSpace(query.ConversationID) { if strings.TrimSpace(query.ConversationID) != "" && message.ConversationID != strings.TrimSpace(query.ConversationID) {
return false return false
@@ -93,6 +112,32 @@ func receiveMessageMatchesQuery(message Message, query ReceiveMessagesQuery) boo
return false return false
} }
func receiveMessageMatchesSince(message Message, since time.Time, hasSince bool) bool {
if !hasSince {
return true
}
messageTime, ok := packetMessageTimestamp(message.Timestamp)
if !ok {
return false
}
return !messageTime.Before(since)
}
func packetMessageTimestamp(value string) (time.Time, bool) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return time.Time{}, false
}
epoch, err := strconv.ParseInt(trimmed, 10, 64)
if err != nil {
return time.Time{}, false
}
if epoch > 1_000_000_000_000 {
return time.UnixMilli(epoch).UTC(), true
}
return time.Unix(epoch, 0).UTC(), true
}
func normalizePacketMessage(msg packetlog.Message) Message { func normalizePacketMessage(msg packetlog.Message) Message {
sender := bareJID(msg.From) sender := bareJID(msg.From)
receiver := bareJID(msg.To) receiver := bareJID(msg.To)

View File

@@ -94,6 +94,47 @@ func TestEncryptedPacketLogSourceFiltersByConversationAndQuery(t *testing.T) {
} }
} }
func TestEncryptedPacketLogSourceFiltersBySince(t *testing.T) {
first := `--------------------------------------------------------------------------------------------------------------------------------------------
2026/7/7 15:30:07
<message id="msg-before" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
<body>before threshold</body>
<received xmlns="urn:xmpp:receipts" id="receipt-before" type="1" stamp="" />
<subject>TASK_UPDATE</subject>
<isphere xmlns="isphere.im" type="1001" sendtime="1783423807000" version="1" />
<readed />
</message>`
second := `--------------------------------------------------------------------------------------------------------------------------------------------
2026/7/7 15:31:07
<message id="msg-after" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
<body>after threshold</body>
<received xmlns="urn:xmpp:receipts" id="receipt-after" type="1" stamp="" />
<subject>TASK_UPDATE</subject>
<isphere xmlns="isphere.im" type="1001" sendtime="1783423867000" version="1" />
<readed />
</message>`
source := EncryptedPacketLogSource{Lines: []string{
encryptPacketLineForTest(t, first),
encryptPacketLineForTest(t, second),
}}
got, err := source.ReceiveMessages(context.Background(), ReceiveMessagesQuery{
Since: "2026-07-07T11:31:00Z",
Limit: 10,
})
if err != nil {
t.Fatalf("ReceiveMessages returned error: %v", err)
}
if len(got.Messages) != 1 || got.Messages[0].ID != "msg-after" {
t.Fatalf("messages = %+v, want only msg-after", got.Messages)
}
_, err = source.ReceiveMessages(context.Background(), ReceiveMessagesQuery{Since: "not-a-time"})
if err == nil {
t.Fatalf("ReceiveMessages accepted invalid since")
}
}
func encryptPacketLineForTest(t *testing.T, plaintext string) string { func encryptPacketLineForTest(t *testing.T, plaintext string) string {
t.Helper() t.Helper()
block, err := des.NewCipher([]byte("hyhccdtm")) block, err := des.NewCipher([]byte("hyhccdtm"))

View File

@@ -20,6 +20,7 @@ type ReceiveMessagesSource interface {
type ReceiveMessagesArgs struct { type ReceiveMessagesArgs struct {
ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"` ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"`
Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"` Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"`
Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp; return messages at or after this time when packet timestamps are available"`
Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"`
} }
@@ -36,6 +37,7 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource)
result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{ result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{
ConversationID: input.ConversationID, ConversationID: input.ConversationID,
Query: input.Query, Query: input.Query,
Since: input.Since,
Limit: input.Limit, Limit: input.Limit,
}) })
if err != nil { if err != nil {

View File

@@ -42,7 +42,7 @@ func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) {
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameReceiveMessages, Name: ToolNameReceiveMessages,
Arguments: map[string]any{"conversation_id": "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou", "query": "REPORT", "limit": 5}, Arguments: map[string]any{"conversation_id": "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou", "query": "REPORT", "since": "2026-07-07T11:30:00Z", "limit": 5},
}) })
if err != nil { if err != nil {
t.Fatalf("call %s: %v", ToolNameReceiveMessages, err) t.Fatalf("call %s: %v", ToolNameReceiveMessages, err)
@@ -50,8 +50,8 @@ func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) {
if callResult.IsError { if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult) t.Fatalf("call result is error: %+v", callResult)
} }
if len(fake.queries) != 1 || fake.queries[0].Limit != 5 || fake.queries[0].ConversationID != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" || fake.queries[0].Query != "REPORT" { if len(fake.queries) != 1 || fake.queries[0].Limit != 5 || fake.queries[0].ConversationID != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" || fake.queries[0].Query != "REPORT" || fake.queries[0].Since != "2026-07-07T11:30:00Z" {
t.Fatalf("queries = %+v, want conversation_id/query/limit", fake.queries) t.Fatalf("queries = %+v, want conversation_id/query/since/limit", fake.queries)
} }
var decoded struct { var decoded struct {

View File

@@ -301,6 +301,7 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int, int) {
callResult, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{ callResult, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{
"conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou",
"query": "report", "query": "report",
"since": "2026-07-07T11:30:00Z",
"limit": 5, "limit": 5,
}}) }})
if err != nil { if err != nil {
@@ -425,6 +426,7 @@ func verifyConfiguredDirectorySource(ctx context.Context) (int, int, int, int) {
callResult, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{ callResult, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{
"conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou",
"query": "dir-report", "query": "dir-report",
"since": "2026-07-07T11:30:00Z",
"limit": 5, "limit": 5,
}}) }})
if err != nil { if err != nil {