Files
openchamber/packages/web/server/lib/session-knowledge/routes.js
T

108 lines
4.5 KiB
JavaScript

/**
* What a session still owes in project knowledge, and the record that it was
* delivered.
*
* Two calls rather than one, because only the sender knows whether the message
* carrying the block actually went out. Handing over the text and recording it
* as delivered in the same request would leave a failed send believing the
* agent has context it never received.
*
* The body parser is attached per route: there is no global one, because the
* generic OpenCode proxy needs an unread request stream.
*/
import express from 'express';
const parseJsonBody = express.json({ limit: '1mb' });
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
const asNonEmptyString = (value) => (
typeof value === 'string' && value.trim().length > 0 ? value.trim() : ''
);
export const registerSessionKnowledgeRoutes = (app, dependencies) => {
const { sessionKnowledgeRuntime } = dependencies;
/**
* Answers with the text to attach and the signature to report back once it
* has gone. An empty text means the session is already carrying it.
*/
app.get('/api/session-knowledge', async (req, res) => {
const directory = asNonEmptyString(req.query.directory);
const sessionId = asNonEmptyString(req.query.sessionId);
if (!directory) {
return res.status(400).json({ error: 'directory is required' });
}
try {
const pending = sessionId
? await sessionKnowledgeRuntime.resolvePendingForSession(sessionId, directory)
// A session that does not exist yet — a draft about to be created —
// has been told nothing, so everything is still owed.
: await sessionKnowledgeRuntime.resolvePending(directory, '');
return res.json(pending);
} catch (error) {
// Never fails the caller's send: a message without its background is far
// better than no message at all.
return res.json({ text: '', signature: '', unavailable: true, reason: error?.message ?? 'unknown' });
}
});
/** Counts and names for the work status panel; assembles no text. */
app.get('/api/session-knowledge/summary', async (req, res) => {
const directory = asNonEmptyString(req.query.directory);
if (!directory) {
return res.json({ notes: [], plans: [], memory: { global: 0, project: 0 } });
}
const sessionId = asNonEmptyString(req.query.sessionId);
try {
return res.json(sessionId
? await sessionKnowledgeRuntime.collectSummaryForSession(sessionId, directory)
: await sessionKnowledgeRuntime.collectSummary(directory));
} catch {
// A panel that cannot read this shows nothing rather than an error.
return res.json({ notes: [], plans: [], memory: { global: 0, project: 0 } });
}
});
app.post('/api/session-knowledge/pin', parseJsonBody, async (req, res) => {
const body = req.body;
if (!isRecord(body)) return res.status(400).json({ error: 'Body must be an object' });
const sessionId = asNonEmptyString(body.sessionId);
const directory = asNonEmptyString(body.directory);
const id = asNonEmptyString(body.id);
const kind = body.kind === 'note' || body.kind === 'plan' ? body.kind : '';
if (!sessionId || !directory || !id || !kind || typeof body.pinned !== 'boolean') {
return res.status(400).json({ error: 'sessionId, directory, kind, id and pinned are required' });
}
try {
return res.json({ pins: await sessionKnowledgeRuntime.setPin(sessionId, directory, kind, id, body.pinned) });
} catch (error) {
return res.status(500).json({ error: error?.message ?? 'Unable to update pin' });
}
});
app.post('/api/session-knowledge/delivered', parseJsonBody, async (req, res) => {
const body = req.body;
if (!isRecord(body)) {
return res.status(400).json({ error: 'Body must be an object' });
}
const sessionId = asNonEmptyString(body.sessionId);
const directory = asNonEmptyString(body.directory);
const signature = asNonEmptyString(body.signature);
if (!sessionId || !directory || !signature) {
return res.status(400).json({ error: 'sessionId, directory and signature are required' });
}
try {
await sessionKnowledgeRuntime.recordDelivered(sessionId, directory, signature);
return res.json({ recorded: true });
} catch (error) {
// The message is already sent; failing here only means the block may be
// sent once more, which is far better than reporting the send as failed.
return res.json({ recorded: false, reason: error?.message ?? 'unknown' });
}
});
};