Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4048,6 +4048,39 @@ if (_isMain) {
})
process.stdout.write(`stored compact summary: memory id ${id}\n`)

} else if (hasFlag('--record-conversation')) {
// The per-turn counterpart to scripts/transcript-sweep.mjs: a hook pipes one
// turn in on stdin as it happens, instead of the sweep catching it later.
//
// This branch has now been dropped by a refactor twice, and both times the
// symptom was the same: the CLI falls through to usage, exits 1, and turns
// silently stop entering the conversations table. The first loss ran 17 days
// before anyone connected the ~150 daily engram_failed warnings to it.
//
// hasFlag, not getFlag: callers pass the flag with no value, so getFlag
// would swallow the next token (--platform).
const chunks = []
for await (const chunk of process.stdin) chunks.push(chunk)
const content = Buffer.concat(chunks).toString('utf-8')
if (!content || content.trim().length === 0) {
process.stderr.write('Error: --record-conversation requires content via stdin\n')
process.exit(1)
}
const id = await recordConversationAsync({
platform: getFlag('--platform') || 'claude-code',
chatId: getFlag('--chat-id') || 'unknown',
messageId: getFlag('--message-id') || null,
fromId: getFlag('--from-id') || null,
fromName: getFlag('--from-name') || '',
role: getFlag('--role') || 'user',
content,
isReply: hasFlag('--is-reply'),
replyToId: getFlag('--reply-to-id') || null,
})
// id === null means UNIQUE-dedup (already recorded) — still a success for
// the caller (exit 0), not a failure.
process.stdout.write(`recorded: ${id || 'dedup'}\n`)

} else if (getFlag('--compress') !== null) {
const chatId = getFlag('--compress')
const days = parseInt(getFlag('--days') || '30', 10)
Expand Down Expand Up @@ -4083,6 +4116,8 @@ if (_isMain) {
' node index.mjs --context "query" Build injection context',
' node index.mjs --recall "query" Recall memory list',
' node index.mjs --recall "" --limit 20 List recent 20 memories',
' node index.mjs --record-conversation Record a conversation turn (content via stdin)',
' [--platform P] [--chat-id C] [--role user|assistant] [--from-name N] [--from-id I] [--is-reply]',
' node index.mjs --store "content" Manually store a memory',
' [--importance 1-10] [--category general|people|project|...]',
' [--type working|short_term|long_term|permanent]',
Expand Down
29 changes: 27 additions & 2 deletions mcp-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ if (existsSync(envPath)) {
readFileSync(envPath, 'utf-8').split('\n').forEach(line => {
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*?)\r?$/)
// Existing env wins — launcher-set values still override the file.
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].trim()
if (m && process.env[m[1]] === undefined) {
process.env[m[1]] = m[2].trim()
} else if (m && m[2].trim() === '') {
// ...except an explicitly empty value, which CLEARS the variable. Without
// this, `MNEME_QUARANTINE_HOSTS=` cannot turn quarantine back off once
// something upstream has set it — the file says off, the env says on, and
// writes keep landing in a table recall never reads. "Existing env wins"
// is right for supplying a value and wrong for withdrawing one.
delete process.env[m[1]]
}
})
}

Expand Down Expand Up @@ -604,7 +613,23 @@ if (useHttp) {

const sessionId = req.headers['mcp-session-id']
let entry = sessionId ? sessions.get(sessionId) : null
if (entry) entry.lastUsed = Date.now() // 复用 session:刷新活跃时间
if (entry) {
entry.lastUsed = Date.now() // 复用 session:刷新活跃时间
// A session's host is bound at creation and every write on it is stamped
// with that host. If a later request on the same session presents a token
// mapping to a different host, the binding and the evidence disagree —
// exactly the case channel-derived provenance exists to prevent. Reject
// under enforce; keep the binding but say so loudly otherwise, because
// the silent version is provenance drifting with no signal at all.
if (auth.authed && entry.host !== auth.host) {
if (AUTH_MODE === 'enforce') {
res.writeHead(401, { 'Content-Type': 'text/plain' })
res.end(`Unauthorized: session bound to host '${entry.host}', token maps to '${auth.host}'`)
return
}
console.error(`[mneme] host mismatch on session ${String(sessionId).slice(0, 8)}: bound=${entry.host}, token=${auth.host} (keeping binding)`)
}
}

if (!entry) {
// New session: open transport + connect a fresh server instance
Expand Down
Loading