diff --git a/addons/discord/transport/gateway_test.go b/addons/discord/transport/gateway_test.go
index 4b9c9b6155..f347ecb6f4 100644
--- a/addons/discord/transport/gateway_test.go
+++ b/addons/discord/transport/gateway_test.go
@@ -106,6 +106,18 @@ var _ = Describe("the Gateway link", func() {
Expect(got[1].Mention).To(BeTrue())
})
+ It("marks a message of a server, a channel or a thread as not direct", func() {
+ connected(f, g)
+ f.dispatch("THREAD_CREATE", map[string]any{"id": threadI, "type": 11, "parent_id": chanID, "guild_id": guildID, "newly_created": true})
+ f.dispatch("MESSAGE_CREATE", msg{channel: chanID, guild: guildID, sender: userID, text: "in the channel", mentionBot: true}.payload())
+ f.dispatch("MESSAGE_CREATE", msg{channel: threadI, guild: guildID, sender: userID, text: "in the thread", mentionBot: true}.payload())
+ f.dispatch("MESSAGE_CREATE", msg{channel: dmID, sender: userID, text: "in a direct message"}.payload())
+ got := next(g, 1, 3)
+ Expect(got).To(HaveLen(3))
+ Expect([]bool{got[0].Direct, got[1].Direct, got[2].Direct}).To(Equal([]bool{false, false, true}))
+ Expect(got[1].Thread).To(Equal(threadI))
+ })
+
It("ignores every message that a bot, a webhook or Discord wrote, and the bot's own", func() {
connected(f, g)
f.dispatch("MESSAGE_CREATE", msg{channel: dmID, sender: botID, text: "my own"}.payload())
diff --git a/addons/slack/transport/runtime_test.go b/addons/slack/transport/runtime_test.go
index 64bba8d426..d02d2d44cb 100644
--- a/addons/slack/transport/runtime_test.go
+++ b/addons/slack/transport/runtime_test.go
@@ -91,6 +91,25 @@ var _ = Describe("the connector route", func() {
Expect(pollUntil(r, out.NextOffset, 1).Updates[0].Thread).To(Equal("500.5"))
})
+ It("marks only a direct message as direct: a group message, a channel mention and a thread are not", func() {
+ f := fake()
+ r := runtimeOf(f)
+ call(r, contract.ConnectorRequest{Op: "poll", Offset: 1})
+ Eventually(func() bool { return f.Connected(appToken) }, 5*time.Second).Should(BeTrue())
+ // A conversation of several people is not read unless the bot is named,
+ // and what is read is never direct.
+ f.Push(appToken, map[string]any{"type": "message", "channel_type": "mpim", "channel": "G1", "user": "U1", "text": "talking", "ts": "1.1"})
+ f.Push(appToken, map[string]any{"type": "message", "channel_type": "group", "channel": "G2", "user": "U1", "text": "talking", "ts": "1.2"})
+ mention := slacktest.Mention("G1", "U1", "UBOT", "hello group", "")
+ f.Push(appToken, mention)
+ // A direct message inside a thread is still direct.
+ f.Push(appToken, map[string]any{"type": "message", "channel_type": "im", "channel": "D1", "user": "U1", "text": "in a thread", "ts": "2.2", "thread_ts": "2.1"})
+ out := pollUntil(r, 1, 2)
+ Expect(out.Updates).To(HaveLen(2))
+ Expect([]any{out.Updates[0].Chat, out.Updates[0].Direct, out.Updates[0].Mention}).To(Equal([]any{"G1", false, true}))
+ Expect([]any{out.Updates[1].Chat, out.Updates[1].Direct, out.Updates[1].Thread}).To(Equal([]any{"D1", true, "2.1"}))
+ })
+
It("waits for an event during a long poll", func() {
f := fake()
r := runtimeOf(f)
diff --git a/addons/telegram/transport/botapi_test.go b/addons/telegram/transport/botapi_test.go
index d31dd178f5..c7ffe22558 100644
--- a/addons/telegram/transport/botapi_test.go
+++ b/addons/telegram/transport/botapi_test.go
@@ -4,6 +4,7 @@ package transport_test
import (
"context"
"errors"
+ "fmt"
"net/http"
"strings"
@@ -43,6 +44,25 @@ var _ = Describe("the Bot API client", func() {
Expect(c.Path).To(ContainSubstring("/bot" + secretToken + "/"))
})
+ DescribeTable("marks a conversation as direct only for a private chat",
+ func(chatType string, direct bool) {
+ f := newFakeBot()
+ f.reply["getUpdates"] = func(map[string]any) (int, string) {
+ return 200, fmt.Sprintf(`{"ok":true,"result":[
+ {"update_id":10,"message":{"chat":{"id":-300,"type":%q},"from":{"id":7,"first_name":"Bo"},"text":"hi"}}]}`, chatType)
+ }
+ ups, err := f.client().Updates(ctx, secretToken, 10, 25)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(ups).To(HaveLen(1))
+ Expect(ups[0].Direct).To(Equal(direct), chatType)
+ },
+ Entry("private", "private", true),
+ Entry("group", "group", false),
+ Entry("supergroup", "supergroup", false),
+ Entry("a type nobody knows counts as a group", "forum", false),
+ Entry("no type counts as a group", "", false),
+ )
+
DescribeTable("types an error and keeps the token out of it",
func(code int, body string, check func(error) bool) {
f := newFakeBot()
diff --git a/core/http/endpoints/studio/agents.go b/core/http/endpoints/studio/agents.go
index 62246b68e3..db8d70f9fa 100644
--- a/core/http/endpoints/studio/agents.go
+++ b/core/http/endpoints/studio/agents.go
@@ -77,7 +77,7 @@ func agentOut(a agents.Agent) schema.ChatsAgent {
}
return schema.ChatsAgent{ID: a.ID, Name: a.Name, Description: a.Description, Instructions: a.Instructions, Model: a.Model,
MCPServers: nz(a.MCPServers), Addons: nz(a.Addons), Collections: nz(a.Collections), Skills: nz(a.Skills),
- Network: networkOf(a.Network), Memory: a.Memory, BuiltinsOff: nz(a.BuiltinsOff), BuiltinsOn: nz(a.BuiltinsOn), Builtins: nz(a.Builtins), Connections: a.Connections, Created: a.Created, Updated: a.Updated}
+ Network: networkOf(a.Network), Memory: a.Memory, MemorySave: a.MemorySave, BuiltinsOff: nz(a.BuiltinsOff), BuiltinsOn: nz(a.BuiltinsOn), Builtins: nz(a.Builtins), Connections: a.Connections, Created: a.Created, Updated: a.Updated}
}
// agentIn reads the body. A model the user may not use is refused here, so a
@@ -100,7 +100,7 @@ func (h *Handlers) agentIn(c echo.Context) (a agents.Agent, folders *[]studiosvc
}
}
return agents.Agent{Name: req.Name, Description: req.Description, Instructions: req.Instructions, Model: req.Model,
- MCPServers: req.MCPServers, Addons: req.Addons, Collections: req.Collections, Skills: req.Skills, Network: req.Network, Memory: req.Memory,
+ MCPServers: req.MCPServers, Addons: req.Addons, Collections: req.Collections, Skills: req.Skills, Network: req.Network, Memory: req.Memory, MemorySave: req.MemorySave,
BuiltinsOff: req.BuiltinsOff, BuiltinsOn: req.BuiltinsOn, Builtins: req.Builtins, Connections: req.Connections},
foldersIn(req), true, nil
}
diff --git a/core/http/endpoints/studio/agentsfile.go b/core/http/endpoints/studio/agentsfile.go
index d87475a790..77bfb73bfb 100644
--- a/core/http/endpoints/studio/agentsfile.go
+++ b/core/http/endpoints/studio/agentsfile.go
@@ -58,7 +58,7 @@ func (h *Handlers) ExportAgent(c echo.Context) error {
return failFor(c, err)
}
w := schema.ChatsAgentWrite{Name: a.Name, Description: a.Description, Instructions: a.Instructions, Model: a.Model,
- MCPServers: a.MCPServers, Addons: a.Addons, Collections: a.Collections, Skills: a.Skills, Network: a.Network, Memory: a.Memory,
+ MCPServers: a.MCPServers, Addons: a.Addons, Collections: a.Collections, Skills: a.Skills, Network: a.Network, Memory: a.Memory, MemorySave: a.MemorySave,
BuiltinsOff: a.BuiltinsOff, BuiltinsOn: a.BuiltinsOn, Builtins: a.Builtins}
rows := h.svc.AgentFolderRows(c.Request().Context(), userID(c), []agents.Agent{a})[a.ID]
for i, f := range a.FolderList() {
@@ -147,7 +147,7 @@ func (h *Handlers) ImportAgent(c echo.Context) error {
}
}
a := agents.Agent{Name: def.Name, Description: def.Description, Instructions: def.Instructions, Model: def.Model,
- MCPServers: def.MCPServers, Addons: def.Addons, Collections: def.Collections, Skills: def.Skills, Network: def.Network, Memory: def.Memory,
+ MCPServers: def.MCPServers, Addons: def.Addons, Collections: def.Collections, Skills: def.Skills, Network: def.Network, Memory: def.Memory, MemorySave: def.MemorySave,
BuiltinsOff: def.BuiltinsOff, BuiltinsOn: def.BuiltinsOn, Builtins: def.Builtins}
var rowsIn []studiosvc.AgentFolderWrite
switch {
diff --git a/core/http/endpoints/studio/memory.go b/core/http/endpoints/studio/memory.go
index e2893a421b..ec02eab9df 100644
--- a/core/http/endpoints/studio/memory.go
+++ b/core/http/endpoints/studio/memory.go
@@ -18,7 +18,7 @@ var memoryRules = []errRule{
func failMemory(c echo.Context, err error) error { return mapErr(c, err, memoryRules, failFor) }
func memorySettingsOut(m studiosvc.MemorySettings) schema.ChatsMemorySettings {
- return schema.ChatsMemorySettings{Enabled: m.Enabled, EnabledSet: m.EnabledSet, SaveMode: m.SaveMode,
+ return schema.ChatsMemorySettings{Enabled: m.Enabled, EnabledSet: m.EnabledSet, SaveMode: m.SaveMode, MessagingSave: m.MessagingSave,
SaveModeSet: m.SaveModeSet, Provider: m.Provider, Available: m.Available}
}
@@ -32,7 +32,7 @@ func memoryStateOut(m studiosvc.MemoryReason) schema.ChatsMemoryState {
// GetMemory reads your memory choices.
// @Summary Read your memory settings.
-// @Description Memory is the short notes the assistant reads before it answers and may save. enabled is the switch for all your chats and agents: a private chat (the memory group of a chat, see PUT /api/chats/{id}/builtins), an agent that turns memory off, or this switch off each make memory off in a chat, and the narrowest wins; a saved agent cannot widen your choice. enabled_set is false until you choose; before that memory is on, unless you had disabled the Memory add-on to stop it. save_mode is how a note is saved (ask, auto or never) and applies as the strictest rule with the rules of the host; a chat from a messaging app always asks. Your notes stay and can be listed, exported and deleted while memory is off.
+// @Description Memory is the short notes the assistant reads before it answers and may save. enabled is the switch for all your chats and agents: a private chat (the memory group of a chat, see PUT /api/chats/{id}/builtins), an agent that turns memory off, or this switch off each make memory off in a chat, and the narrowest wins; a saved agent cannot widen your choice. enabled_set is false until you choose; before that memory is on, unless you had disabled the Memory add-on to stop it. save_mode is how a note is saved: auto (save it and tell me), ask (the default), always (always confirm: like ask, nothing can loosen it, and the model cannot undo or forget by itself) or never. It applies as the strictest rule together with the rule of the saved agent (memory_save) and the rules of the host. A chat from a messaging app asks, unless messaging_save is paired_private and the sender is paired and the chat is one-to-one; the row cannot loosen always or never. Your notes stay and can be listed, exported and deleted while memory is off.
// @Tags chats
// @Success 200 {object} schema.ChatsMemorySettings "Your memory settings"
// @Router /api/chats/memory [get]
@@ -46,7 +46,7 @@ func (h *Handlers) GetMemory(c echo.Context) error {
// PutMemory changes your memory choices.
// @Summary Change your memory settings.
-// @Description Sets enabled (memory On or Off for every chat and agent) and save_mode (ask, auto or never). A field left out stays as it is. Off means nothing is put in the prompt, the model has no tool or skill for notes, nothing is proposed or saved (also by triggers and other runs that start by themselves) and the model is told in one line that memory is off. The settings are kept per person with the account, so every frontend of a cluster reads the same answer. 400 for a value that is not allowed.
+// @Description Sets enabled (memory On or Off for every chat and agent), save_mode (auto, ask, always or never) and messaging_save (ask, paired_private or never). A field left out stays as it is. Off means nothing is put in the prompt, the model has no tool or skill for notes, nothing is proposed or saved (also by triggers and other runs that start by themselves) and the model is told in one line that memory is off. The settings are kept per person with the account, so every frontend of a cluster reads the same answer. 400 for a value that is not allowed.
// @Tags chats
// @Accept json
// @Param request body schema.ChatsMemoryRequest true "the choices to change"
@@ -55,11 +55,11 @@ func (h *Handlers) GetMemory(c echo.Context) error {
// @Failure 503 {object} schema.ErrorResponse "The settings cannot be stored on this server"
// @Router /api/chats/memory [put]
func (h *Handlers) PutMemory(c echo.Context) error {
- req, ok := bind[schema.ChatsMemoryRequest](c, "the body must be {enabled, save_mode}")
+ req, ok := bind[schema.ChatsMemoryRequest](c, "the body must be {enabled, save_mode, messaging_save}")
if !ok {
return nil
}
- m, err := h.svc.SetMemory(c.Request().Context(), userID(c), studiosvc.MemoryUpdate{Enabled: req.Enabled, SaveMode: req.SaveMode})
+ m, err := h.svc.SetMemory(c.Request().Context(), userID(c), studiosvc.MemoryUpdate{Enabled: req.Enabled, SaveMode: req.SaveMode, MessagingSave: req.MessagingSave})
if err != nil {
return failMemory(c, err)
}
diff --git a/core/http/react-ui/e2e/agent-memory.spec.js b/core/http/react-ui/e2e/agent-memory.spec.js
index 1bb58ea228..40be405264 100644
--- a/core/http/react-ui/e2e/agent-memory.spec.js
+++ b/core/http/react-ui/e2e/agent-memory.spec.js
@@ -25,10 +25,10 @@ const GROUPS = [
{ source: 'memory', scope: 'agent', key: 'gone', deleted: true, max_bytes: 2048, max_lines: 40, files: [f('Memory/agents/gone/index.md', { index: true, lines: 1 })] },
]
-async function serve(page, { enabled = true, agents = AGENTS, groups = GROUPS } = {}) {
+async function serve(page, { enabled = true, agents = AGENTS, groups = GROUPS, save = 'ask' } = {}) {
const s = await installAgents(page, { agents })
await page.route('**/api/chats/agents/builtin-grants', route => route.fulfill({ json: { grants: GRANTS } }))
- await page.route('**/api/chats/memory', route => route.fulfill({ json: { enabled, enabled_set: true, save_mode: 'ask', save_mode_set: false, provider: 'builtin', available: true } }))
+ await page.route('**/api/chats/memory', route => route.fulfill({ json: { enabled, enabled_set: true, save_mode: save, messaging_save: 'ask', save_mode_set: false, provider: 'builtin', available: true } }))
await page.route('**/api/chats/context-files', route => route.fulfill({ json: { limits: LIMITS, groups } }))
await page.route('**/api/chats/memory/export*', route => route.fulfill({ status: 200, contentType: 'application/zip', body: 'PK' }))
// A delete that says what to do with the notes carries a query, which the shared stub does not match.
@@ -109,6 +109,50 @@ test.describe('Memory section of the agent form', () => {
})
})
+test.describe('The save rule of the agent', () => {
+ const open = async (page, path) => {
+ await page.goto(path)
+ await fold(page, 'Notes').locator('.ag-fold__button').click()
+ }
+
+ test('offers three cards, never auto, says what is in force, and sends nothing until it is changed', async ({ page }) => {
+ const s = await serve(page, { save: 'auto' })
+ await open(page, '/app/agents/a2/edit')
+ const rule = page.getByTestId('agent-memory-rule')
+ await expect(rule.getByRole('radio')).toHaveCount(3)
+ await expect(rule.locator('.am__title')).toHaveText(['Use my setting', 'Always confirm', 'Never'])
+ await expect(rule).toContainText('Currently: Save it and tell me.')
+ await expect(page.getByTestId('agent-memory-rule-use_mine').locator('input')).toBeChecked()
+ await expect(rule.getByText('Save it and tell me', { exact: true })).toHaveCount(0)
+ await expect(page.getByTestId('agent-memory-rule-hint')).toContainText('never less')
+ await shot(page, 'agent-memory-rule', { fullPage: true })
+ await page.getByRole('button', { name: 'Save changes' }).first().click()
+ await expect(page).toHaveURL(/\/app\/agents$/)
+ expect('memory_save' in s.calls.find(c => c.method === 'PUT').body).toBe(false)
+ })
+
+ test('sends always, then never, then use_mine to clear it', async ({ page }) => {
+ const s = await serve(page, { agents: [{ ...AGENTS[1], memory: 'own', memory_save: 'always' }] })
+ await open(page, '/app/agents/a2/edit')
+ await expect(page.getByTestId('agent-memory-rule-always').locator('input')).toBeChecked()
+ await page.getByTestId('agent-memory-rule-never').click()
+ await page.getByRole('button', { name: 'Save changes' }).first().click()
+ await expect(page).toHaveURL(/\/app\/agents$/)
+ expect(s.calls.filter(c => c.method === 'PUT').at(-1).body.memory_save).toBe('never')
+ await open(page, '/app/agents/a2/edit')
+ await page.getByTestId('agent-memory-rule-use_mine').click()
+ await page.getByRole('button', { name: 'Save changes' }).first().click()
+ await expect(page).toHaveURL(/\/app\/agents$/)
+ expect(s.calls.filter(c => c.method === 'PUT').at(-1).body.memory_save).toBe('use_mine')
+ })
+
+ test('is hidden when the agent has no memory', async ({ page }) => {
+ await serve(page, { agents: [{ ...AGENTS[1], memory: 'none' }] })
+ await open(page, '/app/agents/a2/edit')
+ await expect(page.getByTestId('agent-memory-rule')).toHaveCount(0)
+ })
+})
+
test.describe('Deleting an agent that has notes', () => {
const del = async (page, choice) => {
await page.goto('/app/agents')
diff --git a/core/http/react-ui/e2e/memory-off.spec.js b/core/http/react-ui/e2e/memory-off.spec.js
index cdb154c25d..f7984e6751 100644
--- a/core/http/react-ui/e2e/memory-off.spec.js
+++ b/core/http/react-ui/e2e/memory-off.spec.js
@@ -20,8 +20,8 @@ async function shot(page, name, options = {}) {
const LIMITS = { index_lines: 40, index_bytes: 2048, profile_bytes: 4096, turn_bytes: 6144 }
const NOTES = { 'Memory/index.md': '- home: lives in Rome -> home.md\n- units: metric, 24-hour clock -> units.md\n', 'Memory/home.md': 'Rome.\n', 'Memory/units.md': 'Metric.\n' }
-async function serveAccount(page, { enabled = true, save = 'ask', fail = false } = {}) {
- const s = { enabled, save, puts: [], exports: 0, removed: [] }
+async function serveAccount(page, { enabled = true, save = 'ask', messaging = 'ask', fail = false } = {}) {
+ const s = { enabled, save, messaging, puts: [], exports: 0, removed: [] }
await mockChat(page)
await page.route('**/api/chats/memory', (route) => {
const req = route.request()
@@ -31,8 +31,9 @@ async function serveAccount(page, { enabled = true, save = 'ask', fail = false }
if (fail) return route.fulfill({ status: 503, json: { error: { message: 'the memory settings cannot be stored on this server' } } })
if (typeof body.enabled === 'boolean') s.enabled = body.enabled
if (body.save_mode) s.save = body.save_mode
+ if (body.messaging_save) s.messaging = body.messaging_save
}
- return route.fulfill({ json: { enabled: s.enabled, enabled_set: s.puts.length > 0, save_mode: s.save, save_mode_set: false, provider: 'builtin', available: true } })
+ return route.fulfill({ json: { enabled: s.enabled, enabled_set: s.puts.length > 0, save_mode: s.save, messaging_save: s.messaging, save_mode_set: false, provider: 'builtin', available: true } })
})
await page.route('**/api/chats/memory/export*', (route) => { s.exports++; return route.fulfill({ status: 200, contentType: 'application/zip', body: 'PK' }) })
await page.route('**/api/chats/context-files', route => route.fulfill({ json: {
@@ -81,18 +82,47 @@ test.describe('Settings, Memory: the switch and how a note is saved', () => {
await expect(card).toHaveAttribute('data-state', 'on')
})
- test('lists the three save levels, marks the default and stores the one picked', async ({ page }) => {
+ test('lists the four save levels, marks the default and stores the one picked', async ({ page }) => {
const s = await serveAccount(page)
await page.goto('/app/account?tab=memory')
- const radios = page.getByRole('radio')
- await expect(radios).toHaveCount(3)
- await expect(page.getByRole('radio', { name: /Ask me first/ })).toBeChecked()
+ const levels = page.getByRole('radiogroup', { name: 'Saving a note' })
+ await expect(levels.getByRole('radio')).toHaveCount(4)
+ await shot(page, 'memory-settings-default', { fullPage: true })
+ await expect(levels.locator('.ms-level__title')).toHaveText(['Save it and tell me', 'Ask me firstDefault', 'Always confirm', 'Never'])
+ await expect(levels.getByRole('radio', { name: /^Ask me first/ })).toBeChecked()
await expect(page.getByTestId('memory-save-ask')).toContainText('Default')
- await page.getByRole('radio', { name: /Save it and tell me/ }).click()
+ await levels.getByRole('radio', { name: /^Save it and tell me/ }).click()
await expect.poll(() => s.puts).toEqual([{ save_mode: 'auto' }])
- await expect(page.getByRole('radio', { name: /Save it and tell me/ })).toBeChecked()
- await page.getByRole('radio', { name: /Never/ }).click()
+ await expect(levels.getByRole('radio', { name: /^Save it and tell me/ })).toBeChecked()
+ await levels.getByRole('radio', { name: /^Always confirm/ }).click()
+ await expect.poll(() => s.puts.at(-1)).toEqual({ save_mode: 'always' })
+ await expect(levels.getByRole('radio', { name: /^Always confirm/ })).toBeChecked()
+ await expect(page.getByTestId('memory-save-always')).toContainText('nothing can loosen it')
+ await levels.getByRole('radio', { name: /Never/ }).click()
await expect.poll(() => s.puts.at(-1)).toEqual({ save_mode: 'never' })
+ await shot(page, 'memory-settings-levels', { fullPage: true })
+ })
+
+ test('has an In messaging apps row: Ask first by default, stores the choice, and says when it cannot loosen the level', async ({ page }) => {
+ const s = await serveAccount(page)
+ await page.goto('/app/account?tab=memory')
+ const row = page.getByRole('radiogroup', { name: 'In messaging apps' })
+ await expect(row.locator('.ms-level__title')).toHaveText(['Ask me firstDefault', 'Save it for a paired sender in a private chat', 'Never'])
+ await expect(row.getByRole('radio', { name: /^Ask me first/ })).toBeChecked()
+ await expect(page.getByTestId('memory-messaging-paired_private')).toContainText('Never looser than the setting above.')
+ await expect(page.getByTestId('memory-messaging-locked')).toHaveCount(0)
+ await row.getByRole('radio', { name: /paired sender in a private chat/ }).click()
+ await expect.poll(() => s.puts).toEqual([{ messaging_save: 'paired_private' }])
+ await expect(row.getByRole('radio', { name: /paired sender/ })).toBeChecked()
+ await row.getByRole('radio', { name: /^Never/ }).click()
+ await expect.poll(() => s.puts.at(-1)).toEqual({ messaging_save: 'never' })
+ // The levels above and the row are independent controls: picking a level does not touch the row.
+ await page.getByRole('radiogroup', { name: 'Saving a note' }).getByRole('radio', { name: /^Always confirm/ }).click()
+ await expect.poll(() => s.puts.at(-1)).toEqual({ save_mode: 'always' })
+ await expect(page.getByTestId('memory-messaging-locked')).toContainText('Always confirm, so this row cannot save a note at once')
+ await page.getByRole('radiogroup', { name: 'Saving a note' }).getByRole('radio', { name: /^Never/ }).click()
+ await expect(page.getByTestId('memory-messaging-locked')).toContainText('Your setting above is Never')
+ await shot(page, 'memory-settings-messaging', { fullPage: true })
})
test('a choice the server did not store is not shown as stored', async ({ page }) => {
diff --git a/core/http/react-ui/public/locales/de/chats.json b/core/http/react-ui/public/locales/de/chats.json
index b7747a3fa8..63f21a64d5 100644
--- a/core/http/react-ui/public/locales/de/chats.json
+++ b/core/http/react-ui/public/locales/de/chats.json
@@ -889,13 +889,36 @@
"title": "Speichern und Bescheid geben",
"body": "In deinen eigenen Chats wird die Notiz sofort gespeichert. Der Chat zeigt „Im Gedächtnis gespeichert“ mit Rückgängig. Bis zu 5 Notizen pro Antwort. Eine lange Notiz oder eine, die wie ein Geheimnis aussieht, wartet trotzdem auf dich."
},
+ "always": {
+ "title": "Immer bestätigen",
+ "body": "Wie Erst fragen, und nichts kann es lockern. Auch Rückgängig und Vergessen warten auf dich. Keine Einstellung eines Agenten oder einer Messaging-App hebt es auf."
+ },
"never": {
"title": "Nie",
"body": "Der Assistent kann keine Notizen speichern. Er sagt dir, dass das Speichern aus ist."
}
},
"default": "Standard",
- "saveNote": "Chats aus Messaging-Apps fragen immer zuerst. Hintergrundläufe und Trigger speichern nie eine Notiz. Ein gespeicherter Agent darf mehr fragen, aber nie weniger als du hier gewählt hast.",
+ "saveNote": "Hintergrundläufe und Trigger speichern nie eine Notiz. Ein gespeicherter Agent darf mehr fragen, aber nie weniger als du hier gewählt hast.",
+ "messaging": {
+ "title": "In Messaging-Apps",
+ "lede": "Was passiert, wenn in einem Chat einer Messaging-App eine Notiz vorgeschlagen wird, wo der Absender nicht du sein muss.",
+ "ask": {
+ "title": "Erst fragen",
+ "body": "Der Bot fragt im Chat: „Antworte mit Ja zum Speichern.“ Ein gelisteter Absender kann dort antworten. Andere warten auf dich in der App."
+ },
+ "paired_private": {
+ "title": "Für einen gekoppelten Absender im privaten Chat speichern",
+ "body": "Wird nur sofort gespeichert, wenn der Absender gekoppelt ist und der Chat ein Einzelchat ist. Die Notiz geht in diese Unterhaltung, mit Rückgängig. Eine Notiz, die wie ein Geheimnis aussieht, wartet weiter."
+ },
+ "never": {
+ "title": "Nie",
+ "body": "Aus Messaging-Apps werden keine Notizen gespeichert."
+ },
+ "limit": "Nie lockerer als die Einstellung oben.",
+ "lockedAlways": "Deine Einstellung oben ist Immer bestätigen, daher kann diese Zeile keine Notiz sofort speichern.",
+ "lockedNever": "Deine Einstellung oben ist Nie, daher wird aus Messaging-Apps keine Notiz gespeichert."
+ },
"saveFailed": "Deine Auswahl konnte nicht gespeichert werden.",
"loadError": "Die Gedächtnis-Einstellungen konnten nicht geladen werden.",
"notesTitle": "Notizen verwalten",
@@ -970,6 +993,22 @@
"title": "Notizen ebenfalls löschen",
"body": "Sie wandern in den Papierkorb und lassen sich über Dateien wiederherstellen."
}
+ },
+ "rule": {
+ "title": "Wenn er eine Notiz speichern will",
+ "use_mine": {
+ "title": "Meine Einstellung",
+ "body": "Aktuell: {{level}}."
+ },
+ "always": {
+ "title": "Immer bestätigen",
+ "body": "Strenger als meine Einstellung."
+ },
+ "never": {
+ "title": "Nie",
+ "body": "Liest, speichert nie."
+ },
+ "hint": "Ein Agent darf mehr fragen als deine Einstellung, aber nie weniger. „Speichern und Bescheid geben“ gibt es hier nicht. In einem Messaging-Chat geht eine Notiz zuerst in diese Unterhaltung und erreicht die eigenen Notizen dieses Agenten nur von einem gekoppelten Absender."
}
}
}
diff --git a/core/http/react-ui/public/locales/en/chats.json b/core/http/react-ui/public/locales/en/chats.json
index e5b31c380e..9ea59fbdb8 100644
--- a/core/http/react-ui/public/locales/en/chats.json
+++ b/core/http/react-ui/public/locales/en/chats.json
@@ -1211,13 +1211,36 @@
"title": "Save it and tell me",
"body": "In your own chats the note is saved at once. The chat shows \"Saved to memory\" with an Undo. Up to 5 notes a reply. A long note, or one that looks like a secret, still waits for you."
},
+ "always": {
+ "title": "Always confirm",
+ "body": "Like Ask me first, and nothing can loosen it. Undo and forget also wait for you. No agent or messaging setting can override it."
+ },
"never": {
"title": "Never",
"body": "The assistant cannot save notes. It tells you that saving is off."
}
},
"default": "Default",
- "saveNote": "Chats from messaging apps always ask first. Background runs and triggers never save a note. A saved agent can ask more, never less, than you chose here.",
+ "saveNote": "Background runs and triggers never save a note. A saved agent can ask more, never less, than you chose here.",
+ "messaging": {
+ "title": "In messaging apps",
+ "lede": "What happens when a note is proposed in a chat from a messaging app, where the sender may not be you.",
+ "ask": {
+ "title": "Ask me first",
+ "body": "The bot asks in the chat: \"Reply yes to save it.\" A listed sender can answer there. Others wait for you in the app."
+ },
+ "paired_private": {
+ "title": "Save it for a paired sender in a private chat",
+ "body": "Saved at once only when the sender is paired and the chat is one-to-one. The note goes to that conversation, with an Undo. A note that looks like a secret still waits."
+ },
+ "never": {
+ "title": "Never",
+ "body": "No notes are saved from messaging apps."
+ },
+ "limit": "Never looser than the setting above.",
+ "lockedAlways": "Your setting above is Always confirm, so this row cannot save a note at once.",
+ "lockedNever": "Your setting above is Never, so no note is saved from a messaging app."
+ },
"saveFailed": "Could not store your choice.",
"loadError": "Could not load your memory settings.",
"notesTitle": "Manage your notes",
@@ -1292,6 +1315,22 @@
"title": "Delete the notes too",
"body": "They move to the Trash and you can restore them from Files."
}
+ },
+ "rule": {
+ "title": "When it wants to save a note",
+ "use_mine": {
+ "title": "Use my setting",
+ "body": "Currently: {{level}}."
+ },
+ "always": {
+ "title": "Always confirm",
+ "body": "Stricter than my setting."
+ },
+ "never": {
+ "title": "Never",
+ "body": "It reads, never saves."
+ },
+ "hint": "An agent can ask more than your setting, never less. \"Save it and tell me\" is not offered here. In a messaging chat a note goes to that conversation first, and reaches this agent's own notes only from a paired sender."
}
}
}
diff --git a/core/http/react-ui/public/locales/es/chats.json b/core/http/react-ui/public/locales/es/chats.json
index 981f05f2b2..c2562d1410 100644
--- a/core/http/react-ui/public/locales/es/chats.json
+++ b/core/http/react-ui/public/locales/es/chats.json
@@ -889,13 +889,36 @@
"title": "Guardar y avisarme",
"body": "En tus propios chats la nota se guarda al instante. El chat muestra «Guardado en la memoria» con Deshacer. Hasta 5 notas por respuesta. Una nota larga, o una que parece un secreto, sigue esperando tu confirmación."
},
+ "always": {
+ "title": "Confirmar siempre",
+ "body": "Como Preguntarme antes, y nada puede relajarlo. Deshacer y olvidar también esperan tu respuesta. Ningún ajuste de agente ni de mensajería puede anularlo."
+ },
"never": {
"title": "Nunca",
"body": "El asistente no puede guardar notas. Te dice que guardar está desactivado."
}
},
"default": "Predeterminado",
- "saveNote": "Los chats de apps de mensajería siempre preguntan antes. Las ejecuciones en segundo plano y los disparadores nunca guardan una nota. Un agente guardado puede preguntar más, nunca menos, de lo que elijas aquí.",
+ "saveNote": "Las ejecuciones en segundo plano y los disparadores nunca guardan una nota. Un agente guardado puede preguntar más, nunca menos, de lo que elijas aquí.",
+ "messaging": {
+ "title": "En apps de mensajería",
+ "lede": "Qué pasa cuando se propone una nota en un chat de una app de mensajería, donde quien escribe puede no ser tú.",
+ "ask": {
+ "title": "Preguntarme antes",
+ "body": "El bot pregunta en el chat: «Responde sí para guardarla». Un remitente de la lista puede responder allí. Los demás esperan a que lo hagas en la app."
+ },
+ "paired_private": {
+ "title": "Guardar para un remitente emparejado en un chat privado",
+ "body": "Se guarda al instante solo si el remitente está emparejado y el chat es uno a uno. La nota va a esa conversación, con Deshacer. Una nota que parezca un secreto sigue esperando."
+ },
+ "never": {
+ "title": "Nunca",
+ "body": "No se guardan notas desde apps de mensajería."
+ },
+ "limit": "Nunca más permisivo que el ajuste de arriba.",
+ "lockedAlways": "Tu ajuste de arriba es Confirmar siempre, así que esta fila no puede guardar una nota al instante.",
+ "lockedNever": "Tu ajuste de arriba es Nunca, así que no se guarda ninguna nota desde apps de mensajería."
+ },
"saveFailed": "No se pudo guardar tu elección.",
"loadError": "No se pudieron cargar los ajustes de memoria.",
"notesTitle": "Gestionar tus notas",
@@ -970,6 +993,22 @@
"title": "Eliminar también las notas",
"body": "Van a la papelera y puedes restaurarlas desde Archivos."
}
+ },
+ "rule": {
+ "title": "Cuando quiere guardar una nota",
+ "use_mine": {
+ "title": "Usar mi ajuste",
+ "body": "Ahora: {{level}}."
+ },
+ "always": {
+ "title": "Confirmar siempre",
+ "body": "Más estricto que mi ajuste."
+ },
+ "never": {
+ "title": "Nunca",
+ "body": "Lee, nunca guarda."
+ },
+ "hint": "Un agente puede preguntar más que tu ajuste, nunca menos. «Guardar y avisarme» no se ofrece aquí. En un chat de mensajería la nota va primero a esa conversación y llega a las notas propias de este agente solo desde un remitente emparejado."
}
}
}
diff --git a/core/http/react-ui/public/locales/fr/chats.json b/core/http/react-ui/public/locales/fr/chats.json
index c6505a0f62..d5226fb2dd 100644
--- a/core/http/react-ui/public/locales/fr/chats.json
+++ b/core/http/react-ui/public/locales/fr/chats.json
@@ -889,13 +889,36 @@
"title": "Enregistrer et me prévenir",
"body": "Dans vos propres conversations, la note est enregistrée tout de suite. La conversation affiche « Enregistré dans la mémoire » avec Annuler. Jusqu'à 5 notes par réponse. Une note longue, ou qui ressemble à un secret, attend quand même votre accord."
},
+ "always": {
+ "title": "Toujours confirmer",
+ "body": "Comme Me demander d'abord, et rien ne peut l'assouplir. Annuler et oublier attendent aussi votre réponse. Aucun réglage d'agent ou de messagerie ne peut le contourner."
+ },
"never": {
"title": "Jamais",
"body": "L'assistant ne peut pas enregistrer de notes. Il vous dit que l'enregistrement est désactivé."
}
},
"default": "Par défaut",
- "saveNote": "Les conversations des applications de messagerie demandent toujours d'abord. Les exécutions en arrière-plan et les déclencheurs n'enregistrent jamais de note. Un agent enregistré peut demander plus, jamais moins, que ce que vous choisissez ici.",
+ "saveNote": "Les exécutions en arrière-plan et les déclencheurs n'enregistrent jamais de note. Un agent enregistré peut demander plus, jamais moins, que ce que vous choisissez ici.",
+ "messaging": {
+ "title": "Dans les applications de messagerie",
+ "lede": "Ce qui se passe quand une note est proposée dans une conversation d'une application de messagerie, où l'expéditeur n'est pas forcément vous.",
+ "ask": {
+ "title": "Me demander d'abord",
+ "body": "Le bot demande dans la conversation : « Répondez oui pour l'enregistrer. » Un expéditeur de la liste peut y répondre. Les autres attendent votre réponse dans l'application."
+ },
+ "paired_private": {
+ "title": "Enregistrer pour un expéditeur associé dans une conversation privée",
+ "body": "Enregistrée tout de suite seulement si l'expéditeur est associé et la conversation en tête-à-tête. La note va dans cette conversation, avec Annuler. Une note qui ressemble à un secret attend toujours."
+ },
+ "never": {
+ "title": "Jamais",
+ "body": "Aucune note n'est enregistrée depuis les applications de messagerie."
+ },
+ "limit": "Jamais plus souple que le réglage ci-dessus.",
+ "lockedAlways": "Votre réglage ci-dessus est Toujours confirmer : cette ligne ne peut pas enregistrer une note tout de suite.",
+ "lockedNever": "Votre réglage ci-dessus est Jamais : aucune note n'est enregistrée depuis une application de messagerie."
+ },
"saveFailed": "Impossible d'enregistrer votre choix.",
"loadError": "Impossible de charger vos réglages de mémoire.",
"notesTitle": "Gérer vos notes",
@@ -970,6 +993,22 @@
"title": "Supprimer aussi les notes",
"body": "Elles vont dans la corbeille et vous pouvez les restaurer depuis Fichiers."
}
+ },
+ "rule": {
+ "title": "Quand il veut enregistrer une note",
+ "use_mine": {
+ "title": "Utiliser mon réglage",
+ "body": "Actuellement : {{level}}."
+ },
+ "always": {
+ "title": "Toujours confirmer",
+ "body": "Plus strict que mon réglage."
+ },
+ "never": {
+ "title": "Jamais",
+ "body": "Il lit, n'enregistre jamais."
+ },
+ "hint": "Un agent peut demander plus que votre réglage, jamais moins. « Enregistrer et me prévenir » n'est pas proposé ici. Dans une conversation de messagerie, la note va d'abord à cette conversation et n'atteint les notes propres de cet agent que depuis un expéditeur associé."
}
}
}
diff --git a/core/http/react-ui/public/locales/id/chats.json b/core/http/react-ui/public/locales/id/chats.json
index eb72e79954..fb50eed2f7 100644
--- a/core/http/react-ui/public/locales/id/chats.json
+++ b/core/http/react-ui/public/locales/id/chats.json
@@ -889,13 +889,36 @@
"title": "Simpan dan beri tahu saya",
"body": "Di obrolan Anda sendiri, catatan langsung disimpan. Obrolan menampilkan \"Disimpan ke memori\" dengan Urungkan. Hingga 5 catatan per balasan. Catatan yang panjang, atau yang tampak seperti rahasia, tetap menunggu Anda."
},
+ "always": {
+ "title": "Selalu konfirmasi",
+ "body": "Seperti Tanya saya dulu, dan tidak ada yang bisa melonggarkannya. Urungkan dan lupakan juga menunggu Anda. Tidak ada pengaturan agen atau aplikasi pesan yang bisa menggantinya."
+ },
"never": {
"title": "Jangan pernah",
"body": "Asisten tidak dapat menyimpan catatan. Ia akan memberi tahu bahwa penyimpanan nonaktif."
}
},
"default": "Bawaan",
- "saveNote": "Obrolan dari aplikasi pesan selalu bertanya dulu. Proses latar belakang dan pemicu tidak pernah menyimpan catatan. Agen tersimpan boleh bertanya lebih banyak, tidak pernah lebih sedikit, daripada pilihan Anda di sini.",
+ "saveNote": "Proses latar belakang dan pemicu tidak pernah menyimpan catatan. Agen tersimpan boleh bertanya lebih banyak, tidak pernah lebih sedikit, daripada pilihan Anda di sini.",
+ "messaging": {
+ "title": "Di aplikasi pesan",
+ "lede": "Yang terjadi saat sebuah catatan diusulkan di obrolan aplikasi pesan, di mana pengirimnya belum tentu Anda.",
+ "ask": {
+ "title": "Tanya saya dulu",
+ "body": "Bot bertanya di obrolan: \"Balas ya untuk menyimpan.\" Pengirim yang terdaftar bisa menjawab di sana. Yang lain menunggu Anda di aplikasi."
+ },
+ "paired_private": {
+ "title": "Simpan untuk pengirim yang dipasangkan di obrolan pribadi",
+ "body": "Langsung disimpan hanya jika pengirim dipasangkan dan obrolannya satu lawan satu. Catatan masuk ke percakapan itu, dengan Urungkan. Catatan yang mirip rahasia tetap menunggu."
+ },
+ "never": {
+ "title": "Jangan pernah",
+ "body": "Tidak ada catatan yang disimpan dari aplikasi pesan."
+ },
+ "limit": "Tidak pernah lebih longgar dari pengaturan di atas.",
+ "lockedAlways": "Pengaturan Anda di atas adalah Selalu konfirmasi, jadi baris ini tidak bisa langsung menyimpan catatan.",
+ "lockedNever": "Pengaturan Anda di atas adalah Jangan pernah, jadi tidak ada catatan yang disimpan dari aplikasi pesan."
+ },
"saveFailed": "Pilihan Anda tidak dapat disimpan.",
"loadError": "Pengaturan memori Anda tidak dapat dimuat.",
"notesTitle": "Kelola catatan Anda",
@@ -970,6 +993,22 @@
"title": "Hapus juga catatannya",
"body": "Dipindahkan ke Sampah dan dapat dipulihkan dari Berkas."
}
+ },
+ "rule": {
+ "title": "Saat ingin menyimpan catatan",
+ "use_mine": {
+ "title": "Pakai pengaturan saya",
+ "body": "Saat ini: {{level}}."
+ },
+ "always": {
+ "title": "Selalu konfirmasi",
+ "body": "Lebih ketat dari pengaturan saya."
+ },
+ "never": {
+ "title": "Jangan pernah",
+ "body": "Membaca, tidak pernah menyimpan."
+ },
+ "hint": "Agen boleh bertanya lebih banyak dari pengaturan Anda, tidak pernah lebih sedikit. \"Simpan dan beri tahu saya\" tidak ditawarkan di sini. Di obrolan aplikasi pesan, catatan masuk ke percakapan itu dulu, dan sampai ke catatan milik agen ini hanya dari pengirim yang dipasangkan."
}
}
}
diff --git a/core/http/react-ui/public/locales/it/chats.json b/core/http/react-ui/public/locales/it/chats.json
index c4d9231045..45e9d8de2b 100644
--- a/core/http/react-ui/public/locales/it/chats.json
+++ b/core/http/react-ui/public/locales/it/chats.json
@@ -889,13 +889,36 @@
"title": "Salva e avvisami",
"body": "Nelle tue chat la nota viene salvata subito. La chat mostra «Salvato in memoria» con Annulla. Fino a 5 note per risposta. Una nota lunga, o che sembra un segreto, aspetta comunque te."
},
+ "always": {
+ "title": "Conferma sempre",
+ "body": "Come Chiedimi prima, e nulla può allentarlo. Anche annulla e dimentica aspettano te. Nessuna impostazione di agente o di messaggistica può scavalcarlo."
+ },
"never": {
"title": "Mai",
"body": "L'assistente non può salvare note. Ti dice che il salvataggio è disattivato."
}
},
"default": "Predefinito",
- "saveNote": "Le chat delle app di messaggistica chiedono sempre prima. Le esecuzioni in background e i trigger non salvano mai una nota. Un agente salvato può chiedere di più, mai di meno, di quanto scegli qui.",
+ "saveNote": "Le esecuzioni in background e i trigger non salvano mai una nota. Un agente salvato può chiedere di più, mai di meno, di quanto scegli qui.",
+ "messaging": {
+ "title": "Nelle app di messaggistica",
+ "lede": "Cosa succede quando una nota viene proposta in una chat di un'app di messaggistica, dove chi scrive può non essere tu.",
+ "ask": {
+ "title": "Chiedimi prima",
+ "body": "Il bot chiede nella chat: «Rispondi sì per salvarla». Un mittente in elenco può rispondere lì. Gli altri aspettano te nell'app."
+ },
+ "paired_private": {
+ "title": "Salva per un mittente abbinato in una chat privata",
+ "body": "Salvata subito solo se il mittente è abbinato e la chat è uno a uno. La nota va in quella conversazione, con Annulla. Una nota che sembra un segreto aspetta comunque."
+ },
+ "never": {
+ "title": "Mai",
+ "body": "Nessuna nota viene salvata dalle app di messaggistica."
+ },
+ "limit": "Mai più permissivo dell'impostazione qui sopra.",
+ "lockedAlways": "La tua impostazione qui sopra è Conferma sempre, quindi questa riga non può salvare una nota subito.",
+ "lockedNever": "La tua impostazione qui sopra è Mai, quindi nessuna nota viene salvata dalle app di messaggistica."
+ },
"saveFailed": "Non è stato possibile salvare la tua scelta.",
"loadError": "Non è stato possibile caricare le impostazioni della memoria.",
"notesTitle": "Gestisci le tue note",
@@ -970,6 +993,22 @@
"title": "Elimina anche le note",
"body": "Vanno nel Cestino e puoi ripristinarle da File."
}
+ },
+ "rule": {
+ "title": "Quando vuole salvare una nota",
+ "use_mine": {
+ "title": "Usa la mia impostazione",
+ "body": "Ora: {{level}}."
+ },
+ "always": {
+ "title": "Conferma sempre",
+ "body": "Più rigido della mia impostazione."
+ },
+ "never": {
+ "title": "Mai",
+ "body": "Legge, non salva mai."
+ },
+ "hint": "Un agente può chiedere di più della tua impostazione, mai di meno. «Salva e avvisami» qui non è offerto. In una chat di messaggistica la nota va prima in quella conversazione e arriva alle note proprie di questo agente solo da un mittente abbinato."
}
}
}
diff --git a/core/http/react-ui/public/locales/ko/chats.json b/core/http/react-ui/public/locales/ko/chats.json
index 074a04b5d3..b620e119ab 100644
--- a/core/http/react-ui/public/locales/ko/chats.json
+++ b/core/http/react-ui/public/locales/ko/chats.json
@@ -889,13 +889,36 @@
"title": "저장하고 알려주기",
"body": "내 채팅에서는 메모가 바로 저장됩니다. 채팅에 \"메모리에 저장됨\"과 실행 취소가 표시됩니다. 답변당 최대 5개. 긴 메모나 비밀처럼 보이는 메모는 여전히 확인을 기다립니다."
},
+ "always": {
+ "title": "항상 확인",
+ "body": "먼저 물어보기와 같고, 어떤 것도 이를 완화할 수 없습니다. 실행 취소와 삭제도 내 확인을 기다립니다. 에이전트나 메시징 설정으로 바꿀 수 없습니다."
+ },
"never": {
"title": "안 함",
"body": "어시스턴트가 메모를 저장할 수 없습니다. 저장이 꺼져 있다고 알려줍니다."
}
},
"default": "기본값",
- "saveNote": "메시징 앱의 채팅은 항상 먼저 묻습니다. 백그라운드 실행과 트리거는 메모를 저장하지 않습니다. 저장된 에이전트는 여기서 고른 것보다 더 물어볼 수는 있어도 덜 묻지는 않습니다.",
+ "saveNote": "백그라운드 실행과 트리거는 메모를 저장하지 않습니다. 저장된 에이전트는 여기서 고른 것보다 더 물어볼 수는 있어도 덜 묻지는 않습니다.",
+ "messaging": {
+ "title": "메시징 앱에서",
+ "lede": "보낸 사람이 내가 아닐 수 있는 메시징 앱 채팅에서 메모가 제안될 때의 동작입니다.",
+ "ask": {
+ "title": "먼저 물어보기",
+ "body": "봇이 채팅에서 \"저장하려면 예라고 답하세요\"라고 묻습니다. 목록에 있는 보낸 사람은 거기서 답할 수 있습니다. 다른 사람은 앱에서 내 확인을 기다립니다."
+ },
+ "paired_private": {
+ "title": "비공개 채팅에서 페어링된 보낸 사람의 메모는 저장",
+ "body": "보낸 사람이 페어링되어 있고 일대일 채팅일 때만 바로 저장됩니다. 메모는 그 대화에 실행 취소와 함께 들어갑니다. 비밀처럼 보이는 메모는 계속 기다립니다."
+ },
+ "never": {
+ "title": "안 함",
+ "body": "메시징 앱에서는 메모를 저장하지 않습니다."
+ },
+ "limit": "위 설정보다 느슨해지지 않습니다.",
+ "lockedAlways": "위 설정이 항상 확인이므로 이 항목은 메모를 바로 저장할 수 없습니다.",
+ "lockedNever": "위 설정이 안 함이므로 메시징 앱에서는 메모가 저장되지 않습니다."
+ },
"saveFailed": "선택을 저장하지 못했습니다.",
"loadError": "메모리 설정을 불러오지 못했습니다.",
"notesTitle": "메모 관리",
@@ -970,6 +993,22 @@
"title": "메모도 삭제",
"body": "휴지통으로 이동하며 파일에서 복원할 수 있습니다."
}
+ },
+ "rule": {
+ "title": "메모를 저장하려 할 때",
+ "use_mine": {
+ "title": "내 설정 사용",
+ "body": "현재: {{level}}."
+ },
+ "always": {
+ "title": "항상 확인",
+ "body": "내 설정보다 엄격합니다."
+ },
+ "never": {
+ "title": "안 함",
+ "body": "읽기만 하고 저장하지 않습니다."
+ },
+ "hint": "에이전트는 내 설정보다 더 물어볼 수는 있어도 덜 묻지는 않습니다. \"저장하고 알려주기\"는 여기서 제공되지 않습니다. 메시징 채팅에서는 메모가 먼저 그 대화에 들어가고, 페어링된 보낸 사람일 때만 이 에이전트 자기 메모에 닿습니다."
}
}
}
diff --git a/core/http/react-ui/public/locales/pt-BR/chats.json b/core/http/react-ui/public/locales/pt-BR/chats.json
index 3702f1852c..cc630faf93 100644
--- a/core/http/react-ui/public/locales/pt-BR/chats.json
+++ b/core/http/react-ui/public/locales/pt-BR/chats.json
@@ -889,13 +889,36 @@
"title": "Salvar e me avisar",
"body": "Nos seus próprios chats a nota é salva na hora. O chat mostra \"Salvo na memória\" com Desfazer. Até 5 notas por resposta. Uma nota longa, ou que parece um segredo, ainda espera por você."
},
+ "always": {
+ "title": "Sempre confirmar",
+ "body": "Como Perguntar antes, e nada pode afrouxá-lo. Desfazer e esquecer também esperam por você. Nenhuma configuração de agente ou de mensagens pode sobrepô-lo."
+ },
"never": {
"title": "Nunca",
"body": "O assistente não pode salvar notas. Ele diz que salvar está desativado."
}
},
"default": "Padrão",
- "saveNote": "Chats de apps de mensagens sempre perguntam antes. Execuções em segundo plano e gatilhos nunca salvam uma nota. Um agente salvo pode perguntar mais, nunca menos, do que você escolheu aqui.",
+ "saveNote": "Execuções em segundo plano e gatilhos nunca salvam uma nota. Um agente salvo pode perguntar mais, nunca menos, do que você escolheu aqui.",
+ "messaging": {
+ "title": "Em apps de mensagens",
+ "lede": "O que acontece quando uma nota é proposta em um chat de app de mensagens, onde quem escreve pode não ser você.",
+ "ask": {
+ "title": "Perguntar antes",
+ "body": "O bot pergunta no chat: \"Responda sim para salvar.\" Um remetente da lista pode responder lá. Os outros esperam por você no app."
+ },
+ "paired_private": {
+ "title": "Salvar para um remetente pareado em chat privado",
+ "body": "Salva na hora só se o remetente estiver pareado e o chat for individual. A nota vai para essa conversa, com Desfazer. Uma nota que pareça um segredo continua esperando."
+ },
+ "never": {
+ "title": "Nunca",
+ "body": "Nenhuma nota é salva a partir de apps de mensagens."
+ },
+ "limit": "Nunca mais frouxo que a configuração acima.",
+ "lockedAlways": "Sua configuração acima é Sempre confirmar, então esta linha não pode salvar uma nota na hora.",
+ "lockedNever": "Sua configuração acima é Nunca, então nenhuma nota é salva a partir de apps de mensagens."
+ },
"saveFailed": "Não foi possível salvar sua escolha.",
"loadError": "Não foi possível carregar suas configurações de memória.",
"notesTitle": "Gerenciar suas notas",
@@ -970,6 +993,22 @@
"title": "Excluir as notas também",
"body": "Vão para a Lixeira e você pode restaurá-las em Arquivos."
}
+ },
+ "rule": {
+ "title": "Quando quer salvar uma nota",
+ "use_mine": {
+ "title": "Usar minha configuração",
+ "body": "Agora: {{level}}."
+ },
+ "always": {
+ "title": "Sempre confirmar",
+ "body": "Mais rígido que minha configuração."
+ },
+ "never": {
+ "title": "Nunca",
+ "body": "Lê, nunca salva."
+ },
+ "hint": "Um agente pode perguntar mais que a sua configuração, nunca menos. \"Salvar e me avisar\" não é oferecido aqui. Em um chat de mensagens, a nota vai primeiro para essa conversa e só chega às notas próprias deste agente vinda de um remetente pareado."
}
}
}
diff --git a/core/http/react-ui/public/locales/zh-CN/chats.json b/core/http/react-ui/public/locales/zh-CN/chats.json
index 7f065e30cc..f9cbbb9428 100644
--- a/core/http/react-ui/public/locales/zh-CN/chats.json
+++ b/core/http/react-ui/public/locales/zh-CN/chats.json
@@ -889,13 +889,36 @@
"title": "保存并告知我",
"body": "在你自己的聊天中,笔记会立即保存。聊天会显示“已保存到记忆”和撤销。每次回复最多 5 条。过长或看起来像机密的笔记仍需你确认。"
},
+ "always": {
+ "title": "始终确认",
+ "body": "与“先问我”相同,且任何设置都不能放宽它。撤销和忘记也要等你确认。智能体或消息应用的设置都不能覆盖它。"
+ },
"never": {
"title": "从不",
"body": "助手不能保存笔记,并会告诉你保存已关闭。"
}
},
"default": "默认",
- "saveNote": "来自消息应用的聊天总是先询问。后台运行和触发器从不保存笔记。已保存的智能体可以比这里的选择问得更多,但不会更少。",
+ "saveNote": "后台运行和触发器从不保存笔记。已保存的智能体可以比这里的选择问得更多,但不会更少。",
+ "messaging": {
+ "title": "在消息应用中",
+ "lede": "当消息应用的聊天中有人建议保存笔记、而发送者不一定是你时会发生什么。",
+ "ask": {
+ "title": "先问我",
+ "body": "机器人会在聊天中询问:“回复“是”即可保存。”名单中的发送者可以在那里回答,其他人要等你在应用中确认。"
+ },
+ "paired_private": {
+ "title": "在私聊中为已配对的发送者直接保存",
+ "body": "只有发送者已配对且聊天是一对一时才会立即保存。笔记会存入该对话,并可撤销。看起来像机密的笔记仍然要等待。"
+ },
+ "never": {
+ "title": "从不",
+ "body": "不会从消息应用保存任何笔记。"
+ },
+ "limit": "永远不会比上面的设置更宽松。",
+ "lockedAlways": "你上面的设置是“始终确认”,所以这一项不能立即保存笔记。",
+ "lockedNever": "你上面的设置是“从不”,所以不会从消息应用保存任何笔记。"
+ },
"saveFailed": "无法保存你的选择。",
"loadError": "无法加载你的记忆设置。",
"notesTitle": "管理你的笔记",
@@ -970,6 +993,22 @@
"title": "同时删除笔记",
"body": "笔记会移到回收站,可从“文件”中恢复。"
}
+ },
+ "rule": {
+ "title": "当它想保存笔记时",
+ "use_mine": {
+ "title": "使用我的设置",
+ "body": "当前:{{level}}。"
+ },
+ "always": {
+ "title": "始终确认",
+ "body": "比我的设置更严格。"
+ },
+ "never": {
+ "title": "从不",
+ "body": "只读取,从不保存。"
+ },
+ "hint": "智能体可以比你的设置问得更多,但不会更少。这里不提供“保存并告知我”。在消息聊天中,笔记先存入该对话,只有已配对的发送者才能写入这个智能体自己的笔记。"
}
}
}
diff --git a/core/http/react-ui/src/components/MemorySettings.jsx b/core/http/react-ui/src/components/MemorySettings.jsx
index 123a75f4f3..ec9f4d491f 100644
--- a/core/http/react-ui/src/components/MemorySettings.jsx
+++ b/core/http/react-ui/src/components/MemorySettings.jsx
@@ -9,8 +9,17 @@ import './memorysettings.css'
// The ways a note is saved, in the order they are listed. The value is what the
// server stores; the default is the one a person who never chose gets.
export const SAVE_LEVELS = [
- { value: 'ask', isDefault: true },
{ value: 'auto' },
+ { value: 'ask', isDefault: true },
+ { value: 'always' },
+ { value: 'never' },
+]
+
+// The rows for a note proposed in a messaging app. The row can never be looser
+// than the level above when that is Always confirm or Never.
+export const MESSAGING_LEVELS = [
+ { value: 'ask', isDefault: true },
+ { value: 'paired_private' },
{ value: 'never' },
]
@@ -25,6 +34,7 @@ export default function MemorySettings({ addToast, children }) {
const titleId = useId()
const ledeId = useId()
const saveId = useId()
+ const messagingId = useId()
const notesId = useId()
const notes = (
@@ -40,6 +50,8 @@ export default function MemorySettings({ addToast, children }) {
if (!settings) return
{notes}
const on = settings.enabled
+ // Always confirm and Never hold in messaging apps too: the row cannot loosen them.
+ const lockedBy = settings.save_mode === 'always' || settings.save_mode === 'never' ? settings.save_mode : ''
const store = async (body) => {
if (!(await change(body))) addToast?.(t('memorySettings.saveFailed'), 'error')
}
@@ -77,6 +89,27 @@ export default function MemorySettings({ addToast, children }) {
)
})}
+
+
{notes}
diff --git a/core/http/react-ui/src/components/agents/AgentMemoryChoice.jsx b/core/http/react-ui/src/components/agents/AgentMemoryChoice.jsx
index 8dfd848d09..867ee3d9af 100644
--- a/core/http/react-ui/src/components/agents/AgentMemoryChoice.jsx
+++ b/core/http/react-ui/src/components/agents/AgentMemoryChoice.jsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import Icon from '../Icon'
import { Callout } from '../ui'
-import { MEMORY_CHOICES, memorySaves } from '../../utils/chatAgents'
+import { MEMORY_CHOICES, MEMORY_SAVE_RULES, memorySaves } from '../../utils/chatAgents'
import './agentmemory.css'
// The Memory section of the agent form: which notes the agent reads and where
@@ -12,54 +12,78 @@ import './agentmemory.css'
// none: it shows "As before" as a note, with the cards below to pick a clearer
// choice. `userOff` is the person's own switch (read only here): when it is
// off the choice has no effect, and the form says so.
-export default function AgentMemoryChoice({ value, onChange, saves, onSaves, userOff }) {
+export default function AgentMemoryChoice({ value, onChange, saves, onSaves, userOff, saveRule = '', onSaveRule, userLevel }) {
const { t } = useTranslation('chats')
const groupId = useId()
const grantId = useId()
+ const ruleId = useId()
return (
-