mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
a9ee6618fe
* fix(memory): recall archived session generations after reset * test(memory): prove private recall across reset * fix(memory): keep deleted transcripts outside reset recall * fix(memory): reject deleted archives from reset recall * fix(memory): isolate recall across sqlite resets * test(memory): split reset recall coverage * fix(memory): keep reset recall metadata private * fix(memory): keep reset authority scoped --------- Co-authored-by: TheAngryPit <16145902+TheAngryPit@users.noreply.github.com>
558 lines
24 KiB
YAML
558 lines
24 KiB
YAML
title: Remember across a private session reset
|
|
|
|
scenario:
|
|
id: remember-across-reset-private
|
|
surface: session-memory
|
|
risk: high
|
|
coverage:
|
|
primary:
|
|
- session-memory.active-memory-active-recall
|
|
- session-memory.active-memory-recall
|
|
secondary:
|
|
- session-memory.active-memory-qa-channel
|
|
- channels.channel-native-commands
|
|
objective: Verify SQLite reset continuity and pre-reset generation recall without admitting deleted, shared, or other-agent transcripts.
|
|
plugins:
|
|
- active-memory
|
|
gatewayConfigPatch:
|
|
session:
|
|
dmScope: per-channel-peer
|
|
memory:
|
|
search:
|
|
rememberAcrossConversations: true
|
|
agents:
|
|
entries:
|
|
peer:
|
|
identity:
|
|
name: QA Peer
|
|
plugins:
|
|
entries:
|
|
active-memory:
|
|
enabled: true
|
|
config:
|
|
enabled: true
|
|
mode: always
|
|
agents: []
|
|
toolsAllow:
|
|
- memory_search
|
|
logging: true
|
|
persistTranscripts: true
|
|
transcriptDir: qa-remember-across-reset-private
|
|
queryMode: message
|
|
maxSummaryChars: 220
|
|
successCriteria:
|
|
- A real Gateway session reset changes the private lifecycle revision while retaining its durable session id.
|
|
- The pre-reset fact is indexed under the canonical SQLite session identity before the current generation cutoff.
|
|
- Active Memory recalls the pre-reset private fact in the same conversation after the current reset.
|
|
- An indexed deleted private marker, shared marker, and independently indexed peer-agent marker are absent from accepted recall evidence.
|
|
docsRefs:
|
|
- docs/concepts/active-memory.md
|
|
- docs/reference/memory-config.md
|
|
- docs/reference/session-management-compaction.md
|
|
codeRefs:
|
|
- extensions/active-memory/index.ts
|
|
- extensions/memory-core/src/session-search-visibility.ts
|
|
- src/auto-reply/reply/session-reset-command.ts
|
|
- extensions/qa-lab/src/suite-runtime-flow.ts
|
|
- extensions/qa-lab/src/providers/mock-openai/server.ts
|
|
execution:
|
|
kind: flow
|
|
channel: qa-channel
|
|
providerMode: mock-openai
|
|
retryCount: 0
|
|
suiteIsolation: isolated
|
|
isolationReason: Resets and indexes isolated QA and peer-agent transcripts while running an ephemeral Gateway child.
|
|
summary: Reset one private QA conversation, index its canonical SQLite transcript generations, and prove private-only Active Memory recall in that same conversation.
|
|
config:
|
|
requiredProviderMode: mock-openai
|
|
conversationId: remember-reset-private
|
|
deletedConversationId: remember-reset-deleted
|
|
groupConversationId: remember-reset-group
|
|
peerConversationId: remember-reset-peer
|
|
privateFact: lemon pepper wings with blue cheese
|
|
deletedFact: DELETED-RESET-ONLY cinnamon popcorn with chili salt
|
|
groupFact: GROUP-RESET-ONLY loaded nachos with black olives
|
|
peerFact: PEER-RESET-ONLY smoked tofu skewers
|
|
seedMarker: QA-REMEMBER-RESET-SOURCE-SEEDED
|
|
recallPrompt: "Remember across conversations QA check: what snack do I usually want for QA movie night? Reply in one short sentence."
|
|
expectedNeedle: lemon pepper wings with blue cheese
|
|
transcriptDir: qa-remember-across-reset-private
|
|
|
|
flow:
|
|
steps:
|
|
- name: recalls the pre-reset private generation without crossing ownership boundaries
|
|
actions:
|
|
- assert:
|
|
expr: "env.providerMode === config.requiredProviderMode"
|
|
message: this deterministic reset-and-recall proof requires mock-openai
|
|
- call: waitForGatewayHealthy
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- call: waitForQaChannelReady
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- resetTransport: true
|
|
- call: fs.rm
|
|
args:
|
|
- expr: "path.join(env.gateway.workspaceDir, 'MEMORY.md')"
|
|
- force: true
|
|
- call: fs.rm
|
|
args:
|
|
- expr: "path.join(env.gateway.workspaceDir, 'memory', `${formatMemoryDreamingDay(Date.now())}.md`)"
|
|
- force: true
|
|
- set: sourceDelivery
|
|
value:
|
|
expr: "transport.buildAgentDelivery({ target: `dm:${config.conversationId}` })"
|
|
- set: groupDelivery
|
|
value:
|
|
expr: "transport.buildAgentDelivery({ target: `channel:${config.groupConversationId}` })"
|
|
- set: deletedDelivery
|
|
value:
|
|
expr: "transport.buildAgentDelivery({ target: `dm:${config.deletedConversationId}` })"
|
|
- set: peerDelivery
|
|
value:
|
|
expr: "transport.buildAgentDelivery({ target: `dm:${config.peerConversationId}` })"
|
|
- set: sourceSessionKey
|
|
value:
|
|
expr: "buildAgentSessionKey({ agentId: 'qa', channel: sourceDelivery.channel, accountId: transport.accountId, peer: { kind: 'direct', id: sourceDelivery.replyTo }, dmScope: env.cfg.session?.dmScope, identityLinks: env.cfg.session?.identityLinks })"
|
|
- set: groupSessionKey
|
|
value:
|
|
expr: "buildAgentSessionKey({ agentId: 'qa', channel: groupDelivery.channel, accountId: transport.accountId, peer: { kind: 'channel', id: groupDelivery.replyTo } })"
|
|
- set: deletedSessionKey
|
|
value:
|
|
expr: "buildAgentSessionKey({ agentId: 'qa', channel: deletedDelivery.channel, accountId: transport.accountId, peer: { kind: 'direct', id: deletedDelivery.replyTo }, dmScope: env.cfg.session?.dmScope, identityLinks: env.cfg.session?.identityLinks })"
|
|
- set: peerSessionKey
|
|
value:
|
|
expr: "buildAgentSessionKey({ agentId: 'peer', channel: peerDelivery.channel, accountId: transport.accountId, peer: { kind: 'direct', id: peerDelivery.replyTo }, dmScope: env.cfg.session?.dmScope, identityLinks: env.cfg.session?.identityLinks })"
|
|
- set: transcriptRoot
|
|
value:
|
|
expr: "path.join(env.gateway.tempRoot, 'state', 'plugins', 'active-memory', 'transcripts', 'agents', 'qa', config.transcriptDir)"
|
|
- call: fs.rm
|
|
args:
|
|
- ref: transcriptRoot
|
|
- recursive: true
|
|
force: true
|
|
- sendInbound:
|
|
conversation:
|
|
id:
|
|
ref: config.deletedConversationId
|
|
kind: direct
|
|
senderId:
|
|
ref: config.deletedConversationId
|
|
senderName: Remember Reset Deleted Source
|
|
text:
|
|
expr: "`Stable QA movie night usual favorite snack preference: ${config.deletedFact}. This source will be deleted. Acknowledge briefly.`"
|
|
- waitForOutbound:
|
|
conversation:
|
|
id:
|
|
ref: config.deletedConversationId
|
|
kind: direct
|
|
timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- sendInbound:
|
|
conversation:
|
|
id:
|
|
ref: config.conversationId
|
|
kind: direct
|
|
senderId:
|
|
ref: config.conversationId
|
|
senderName: Remember Reset Source
|
|
text:
|
|
expr: "`Stable QA movie night usual favorite snack preference: ${config.privateFact}. Reply exactly: ${config.seedMarker}.`"
|
|
- waitForOutbound:
|
|
conversation:
|
|
id:
|
|
ref: config.conversationId
|
|
kind: direct
|
|
textIncludes:
|
|
ref: config.seedMarker
|
|
timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- sendInbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
senderId: { ref: config.conversationId }
|
|
senderName: Remember Reset Source
|
|
text: "Filler turn one after the saved fact. Reply briefly."
|
|
- waitForOutbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
timeoutMs: { expr: "liveTurnTimeoutMs(env, 60000)" }
|
|
- sendInbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
senderId: { ref: config.conversationId }
|
|
senderName: Remember Reset Source
|
|
text: "Filler turn two after the saved fact. Reply briefly."
|
|
- waitForOutbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
timeoutMs: { expr: "liveTurnTimeoutMs(env, 60000)" }
|
|
- sendInbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
senderId: { ref: config.conversationId }
|
|
senderName: Remember Reset Source
|
|
text: "Filler turn three after the saved fact. Reply briefly."
|
|
- waitForOutbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
timeoutMs: { expr: "liveTurnTimeoutMs(env, 60000)" }
|
|
- sendInbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
senderId: { ref: config.conversationId }
|
|
senderName: Remember Reset Source
|
|
text: "Filler turn four after the saved fact. Reply briefly."
|
|
- waitForOutbound:
|
|
conversation: { id: { ref: config.conversationId }, kind: direct }
|
|
timeoutMs: { expr: "liveTurnTimeoutMs(env, 60000)" }
|
|
- sendInbound:
|
|
conversation:
|
|
id:
|
|
ref: config.groupConversationId
|
|
kind: channel
|
|
title: Remember Reset Group
|
|
senderId: remember-reset-group-member
|
|
senderName: Remember Reset Group Member
|
|
text:
|
|
expr: "`@openclaw Stable QA movie night usual favorite snack preference: ${config.groupFact}. This applies only inside this group. Acknowledge briefly.`"
|
|
- waitForOutbound:
|
|
conversation:
|
|
id:
|
|
ref: config.groupConversationId
|
|
kind: channel
|
|
timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- call: env.gateway.call
|
|
saveAs: peerStarted
|
|
args:
|
|
- agent
|
|
- agentId: peer
|
|
sessionKey:
|
|
ref: peerSessionKey
|
|
idempotencyKey:
|
|
expr: randomUUID()
|
|
message:
|
|
expr: "`Stable QA movie night usual favorite snack preference: ${config.peerFact}. This belongs only to the peer agent. Acknowledge briefly.`"
|
|
deliver: false
|
|
channel:
|
|
expr: peerDelivery.channel
|
|
to:
|
|
expr: "peerDelivery.to ?? `dm:${config.peerConversationId}`"
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 180000)
|
|
- assert:
|
|
expr: "Boolean(peerStarted?.runId)"
|
|
message:
|
|
expr: "`peer agent run did not start: ${JSON.stringify(peerStarted)}`"
|
|
- call: env.gateway.call
|
|
saveAs: peerWaited
|
|
args:
|
|
- agent.wait
|
|
- runId:
|
|
expr: peerStarted.runId
|
|
timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 180000)
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 185000)
|
|
- assert:
|
|
expr: "['ok', 'completed', 'succeeded'].includes(peerWaited?.status) || (peerWaited?.status === 'error' && String(peerWaited?.error ?? '').trim().toLowerCase() === 'completed')"
|
|
message:
|
|
expr: "`peer agent run did not complete: ${JSON.stringify(peerWaited)}`"
|
|
- call: readRawQaSessionStore
|
|
saveAs: seededStore
|
|
args:
|
|
- ref: env
|
|
- call: readRawQaSessionStore
|
|
saveAs: peerStore
|
|
args:
|
|
- ref: env
|
|
- agentId: peer
|
|
- set: seedSession
|
|
value:
|
|
expr: seededStore[sourceSessionKey]
|
|
- set: groupSession
|
|
value:
|
|
expr: seededStore[groupSessionKey]
|
|
- set: deletedSession
|
|
value:
|
|
expr: seededStore[deletedSessionKey]
|
|
- set: peerSession
|
|
value:
|
|
expr: peerStore[peerSessionKey]
|
|
- assert:
|
|
expr: "Boolean(seedSession?.sessionId) && Boolean(deletedSession?.sessionId) && Boolean(groupSession?.sessionId) && Boolean(peerSession?.sessionId)"
|
|
message:
|
|
expr: "`seeded transcript identities missing: ${JSON.stringify({ source: seedSession, group: groupSession, peer: peerSession })}`"
|
|
- call: env.gateway.call
|
|
saveAs: resetResult
|
|
args:
|
|
- sessions.reset
|
|
- key:
|
|
ref: sourceSessionKey
|
|
reason: reset
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- set: resetSession
|
|
value:
|
|
expr: resetResult.entry
|
|
- assert:
|
|
expr: "resetResult?.ok === true && Boolean(resetSession?.lifecycleRevision) && resetSession.lifecycleRevision !== seedSession.lifecycleRevision"
|
|
message:
|
|
expr: "`gateway session reset did not advance lifecycle: ${JSON.stringify({ result: resetResult, before: seedSession?.lifecycleRevision })}`"
|
|
- assert:
|
|
expr: resetSession?.sessionId === seedSession.sessionId
|
|
message:
|
|
expr: "`durable session id changed across reset: ${JSON.stringify({ before: seedSession?.sessionId, after: resetSession?.sessionId })}`"
|
|
- call: readSessionTranscriptSummary
|
|
saveAs: resetTranscript
|
|
args:
|
|
- ref: env
|
|
- ref: sourceSessionKey
|
|
- probeText:
|
|
ref: config.privateFact
|
|
- assert:
|
|
expr: "Number.isInteger(resetTranscript.probeTextEndLine) && Number.isInteger(resetTranscript.resetRecallCutoffLine) && resetTranscript.probeTextEndLine < resetTranscript.resetRecallCutoffLine"
|
|
message:
|
|
expr: "`seed fact was not strictly before the effective SQLite reset cutoff: ${JSON.stringify(resetTranscript)}`"
|
|
- set: canonicalSessionPath
|
|
value:
|
|
expr: "`sessions/qa/${seedSession.sessionId}.jsonl`"
|
|
- call: env.gateway.call
|
|
saveAs: deletedResult
|
|
args:
|
|
- sessions.delete
|
|
- key:
|
|
ref: deletedSessionKey
|
|
expectedSessionId:
|
|
expr: deletedSession.sessionId
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- assert:
|
|
expr: "deletedResult?.deleted === true && deletedResult.archived?.some((entry) => String(entry).includes(`${deletedSession.sessionId}.jsonl.deleted.`))"
|
|
message:
|
|
expr: "`same-agent private deletion did not produce its archive: ${JSON.stringify(deletedResult)}`"
|
|
- set: deletedArchiveName
|
|
value:
|
|
expr: "path.basename(deletedResult.archived.find((entry) => String(entry).includes(`${deletedSession.sessionId}.jsonl.deleted.`)))"
|
|
- call: readConfigSnapshot
|
|
saveAs: preconditionConfig
|
|
args:
|
|
- ref: env
|
|
- set: originalMemorySearch
|
|
value:
|
|
expr: "preconditionConfig.config.memory && typeof preconditionConfig.config.memory === 'object' ? structuredClone(preconditionConfig.config.memory.search) : undefined"
|
|
- call: patchConfig
|
|
args:
|
|
- env:
|
|
ref: env
|
|
patch:
|
|
memory:
|
|
search:
|
|
expr: "{ ...structuredClone(originalMemorySearch ?? {}), sources: ['memory', 'sessions'] }"
|
|
- call: waitForGatewayHealthy
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- call: waitForQaChannelReady
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- call: runQaCli
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- index
|
|
- --agent
|
|
- peer
|
|
- --force
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- call: runQaCli
|
|
saveAs: peerSearch
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- search
|
|
- --agent
|
|
- peer
|
|
- --json
|
|
- --query
|
|
- expr: config.peerFact
|
|
- --max-results
|
|
- "50"
|
|
- --min-score
|
|
- "0"
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
json: true
|
|
- assert:
|
|
expr: "JSON.stringify(peerSearch).includes(config.peerFact) && JSON.stringify(peerSearch).includes(peerSession.sessionId)"
|
|
message:
|
|
expr: "`peer agent index did not contain its marker: ${JSON.stringify(peerSearch)}`"
|
|
- call: runQaCli
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- index
|
|
- --agent
|
|
- qa
|
|
- --force
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
- call: runQaCli
|
|
saveAs: qaSearch
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- search
|
|
- --agent
|
|
- qa
|
|
- --json
|
|
- --query
|
|
- expr: config.privateFact
|
|
- --max-results
|
|
- "50"
|
|
- --min-score
|
|
- "0"
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
json: true
|
|
- set: qaSearchText
|
|
value:
|
|
expr: JSON.stringify(qaSearch)
|
|
- call: runQaCli
|
|
saveAs: groupSearch
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- search
|
|
- --agent
|
|
- qa
|
|
- --json
|
|
- --query
|
|
- expr: config.groupFact
|
|
- --max-results
|
|
- "50"
|
|
- --min-score
|
|
- "0"
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
json: true
|
|
- call: runQaCli
|
|
saveAs: deletedSearch
|
|
args:
|
|
- ref: env
|
|
- - memory
|
|
- search
|
|
- --agent
|
|
- qa
|
|
- --json
|
|
- --query
|
|
- expr: config.deletedFact
|
|
- --max-results
|
|
- "50"
|
|
- --min-score
|
|
- "0"
|
|
- timeoutMs:
|
|
expr: liveTurnTimeoutMs(env, 60000)
|
|
json: true
|
|
- set: deletedSearchText
|
|
value:
|
|
expr: JSON.stringify(deletedSearch)
|
|
- set: groupSearchText
|
|
value:
|
|
expr: JSON.stringify(groupSearch)
|
|
- assert:
|
|
expr: "groupSearchText.includes(config.groupFact) && groupSearchText.includes(groupSession.sessionId)"
|
|
message:
|
|
expr: "`QA index did not contain the seeded group transcript: ${groupSearchText}`"
|
|
- assert:
|
|
expr: "deletedSearchText.includes(config.deletedFact) && deletedSearchText.includes(deletedArchiveName)"
|
|
message:
|
|
expr: "`QA index did not contain the deleted private control: ${deletedSearchText}`"
|
|
- assert:
|
|
expr: "qaSearchText.includes(config.privateFact) && qaSearchText.includes(canonicalSessionPath)"
|
|
message:
|
|
expr: "`QA index did not return the pre-reset private fact at its canonical SQLite path: ${qaSearchText}`"
|
|
- call: patchConfig
|
|
args:
|
|
- env:
|
|
ref: env
|
|
patch:
|
|
memory:
|
|
search:
|
|
expr: "originalMemorySearch === undefined ? null : structuredClone(originalMemorySearch)"
|
|
- call: waitForGatewayHealthy
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- call: waitForQaChannelReady
|
|
args:
|
|
- ref: env
|
|
- 60000
|
|
- call: env.gateway.restartAfterStateMutation
|
|
args:
|
|
- lambda:
|
|
async: true
|
|
expr: await Promise.resolve()
|
|
- call: fs.rm
|
|
args:
|
|
- ref: transcriptRoot
|
|
- recursive: true
|
|
force: true
|
|
- set: requestCursorBeforeRecall
|
|
value:
|
|
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/request-cursor`)).cursor"
|
|
- set: recallOutboundIndex
|
|
value:
|
|
expr: "state.getSnapshot().messages.filter((message) => message.direction === 'outbound').length"
|
|
- sendInbound:
|
|
conversation:
|
|
id:
|
|
ref: config.conversationId
|
|
kind: direct
|
|
senderId:
|
|
ref: config.conversationId
|
|
senderName: Remember Reset Source
|
|
text:
|
|
ref: config.recallPrompt
|
|
- call: waitForOutboundMessage
|
|
saveAs: recallOutbound
|
|
args:
|
|
- ref: state
|
|
- lambda:
|
|
params: [candidate]
|
|
expr: "candidate.direction === 'outbound' && candidate.conversation.id === config.conversationId"
|
|
- expr: liveTurnTimeoutMs(env, 60000)
|
|
- sinceIndex:
|
|
ref: recallOutboundIndex
|
|
- call: waitForCondition
|
|
saveAs: helperTranscriptPath
|
|
args:
|
|
- lambda:
|
|
async: true
|
|
expr: "await (async () => { const entries = (await fs.readdir(transcriptRoot).catch(() => [])).filter((entry) => entry.endsWith('.jsonl')).toSorted(); return entries.length > 0 ? path.join(transcriptRoot, entries.at(-1)) : undefined; })()"
|
|
- expr: liveTurnTimeoutMs(env, 30000)
|
|
- 250
|
|
- call: fs.readFile
|
|
saveAs: helperTranscriptText
|
|
args:
|
|
- ref: helperTranscriptPath
|
|
- utf8
|
|
- set: recallRequests
|
|
value:
|
|
expr: "await fetchJson(`${env.mock.baseUrl}/debug/requests?after=${requestCursorBeforeRecall}`)"
|
|
- assert:
|
|
expr: "normalizeLowercaseStringOrEmpty(recallOutbound.text).includes(normalizeLowercaseStringOrEmpty(config.expectedNeedle))"
|
|
message:
|
|
expr: "`same-conversation post-reset reply missed the pre-reset private fact: ${recallOutbound.text}`"
|
|
- assert:
|
|
expr: "helperTranscriptText.includes('memory_search') && helperTranscriptText.includes(config.privateFact) && helperTranscriptText.includes(seedSession.sessionId)"
|
|
message:
|
|
expr: "`Active Memory helper did not accept the canonical pre-reset private fact: ${helperTranscriptText}`"
|
|
- assert:
|
|
expr: "!helperTranscriptText.includes(config.deletedFact) && !helperTranscriptText.includes(deletedArchiveName) && !helperTranscriptText.includes(config.groupFact) && !helperTranscriptText.includes(groupSession.sessionId) && !helperTranscriptText.includes(config.peerFact) && !helperTranscriptText.includes(peerSession.sessionId)"
|
|
message:
|
|
expr: "`Active Memory helper crossed a shared or peer-agent boundary: ${helperTranscriptText}`"
|
|
- assert:
|
|
expr: "recallRequests.some((request) => String(request.allInputText ?? '').includes('Remember across conversations QA check') && request.plannedToolName === 'memory_search')"
|
|
message: deterministic post-reset recall did not issue memory_search
|
|
detailsExpr: "JSON.stringify({ verdict: 'PASS', scenario: 'remember-across-reset-private', channel: 'qa-channel', provider: env.providerMode, gateway: 'ephemeral-child', lifecycleReset: resetSession.lifecycleRevision !== seedSession.lifecycleRevision, durableSessionRetained: resetSession.sessionId === seedSession.sessionId, canonicalResetBoundary: true, preResetHitIndexed: qaSearchText.includes(canonicalSessionPath), preResetHitAccepted: helperTranscriptText.includes(seedSession.sessionId), deletedTranscriptIndexed: deletedSearchText.includes(deletedArchiveName), deletedTranscriptExcluded: !helperTranscriptText.includes(deletedArchiveName), groupTranscriptIndexed: groupSearchText.includes(groupSession.sessionId), peerTranscriptIndexed: JSON.stringify(peerSearch).includes(peerSession.sessionId), privateRecallDelivered: normalizeLowercaseStringOrEmpty(recallOutbound.text).includes(normalizeLowercaseStringOrEmpty(config.expectedNeedle)), sharedExcluded: !helperTranscriptText.includes(groupSession.sessionId), peerAgentExcluded: !helperTranscriptText.includes(peerSession.sessionId), reply: recallOutbound.text })"
|