Compare commits

..
Author SHA1 Message Date
jason.woltje 4e2a413640 Merge M6: named sessions - persistence and resume
Closes #22, closes #23
2026-09-02 22:08:34 -05:00
jason.woltje be55549700 feat(sessions): named persistent sessions with resume (L1) (#22, #23)
- task schema: optional session (named id) -> persistent session dir at
  dataRoot/sessions/<name>, isolated per name
- pi adapter: --session-dir when declared (ephemeral --no-session stays
  the default otherwise); -c resumes the most recent session when present
- compose passthrough; result.json records session
- fixtures: tasks/session-demo-1.json (teach) + session-demo-2.json (recall)
- E2E: teach -> REMEMBERED + host-side session JSONL; resume -> recalled
  'mosaico' exactly; single continued session file

Closes #22, closes #23
2026-09-02 22:08:34 -05:00
5 changed files with 50 additions and 2 deletions
+13 -1
View File
@@ -18,6 +18,18 @@ if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
cd "$MOSAIC_WORKSPACE"
fi
# Session (M6): persistent named session directory; resume the most recent
# session in that directory when one exists (pi documented flags).
# Default remains ephemeral (--no-session) when no session is declared.
SESSION_FLAGS="--no-session"
if [ -n "${MOSAIC_SESSION_DIR:-}" ]; then
mkdir -p "$MOSAIC_SESSION_DIR"
SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR"
if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then
SESSION_FLAGS="$SESSION_FLAGS -c"
fi
fi
# Capabilities (M5): explicit allowlist or no tools.
TOOLS_FLAG="--no-tools"
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
@@ -30,13 +42,13 @@ TOOLS_FLAG="--no-tools"
# --offline no startup network operations (update checks/telemetry)
exec pi \
--offline \
--no-session \
--no-extensions \
--no-skills \
--no-prompt-templates \
--no-themes \
--no-context-files \
$TOOLS_FLAG \
$SESSION_FLAGS \
--provider "$PI_PROVIDER" \
--model "$PI_MODEL" \
--system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \
+2
View File
@@ -18,6 +18,8 @@ services:
# Workspace + capabilities (set by the task runner; M5)
MOSAIC_WORKSPACE: ${MOSAIC_WORKSPACE:-}
MOSAIC_TOOLS: ${MOSAIC_TOOLS:-}
# Persistent named session dir (set by the task runner; M6)
MOSAIC_SESSION_DIR: ${MOSAIC_SESSION_DIR:-}
# mock adapter only: verbatim response for deterministic seam tests
MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-}
# Documented container auth alternative: provider API key via
+20 -1
View File
@@ -106,7 +106,7 @@ function validateMission(document, file) {
function validateTask(document, file) {
if (!isPlainObject(document)) fail(2, "task must be a JSON object");
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities"], "task");
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities", "session"], "task");
if (document.taskVersion !== 1) {
fail(2, `unsupported taskVersion: ${JSON.stringify(document.taskVersion)} (supported: 1)`);
}
@@ -158,6 +158,17 @@ function validateTask(document, file) {
workspace = document.workspace;
}
// Session (M6): optional named persistent session under
// <dataRoot>/sessions/<name>. Distinct names never share state.
let session = null;
if (document.session !== undefined && document.session !== null) {
if (typeof document.session !== "string" || document.session.length === 0) {
fail(2, 'task "session" must be a non-empty string when present');
}
validateId(document.session, "task session");
session = document.session;
}
// Capabilities (M5): optional tools allowlist mapped by adapters to their
// native permission flags. Absent = no tools.
let tools = null;
@@ -189,6 +200,7 @@ function validateTask(document, file) {
timeoutSeconds,
workspace,
tools,
session,
};
}
@@ -271,6 +283,12 @@ function runTask(taskFile) {
if (workspaceContainerPath) spawnEnv.MOSAIC_WORKSPACE = workspaceContainerPath;
spawnEnv.MOSAIC_TOOLS = task.tools ? task.tools.join(",") : "";
// Session (M6): persistent named session dir, passed as container path.
if (task.session) {
fs.mkdirSync(path.join(resolved.dataRoot, "sessions", task.session), { recursive: true });
spawnEnv.MOSAIC_SESSION_DIR = `/var/lib/mosaic/sessions/${task.session}`;
}
const proc = spawnSync(
"docker",
["compose", "run", "--rm", "-T", "mosaic-agent", task.prompt],
@@ -319,6 +337,7 @@ function runTask(taskFile) {
expectedExact: expected,
workspace: task.workspace,
tools: task.tools,
session: task.session,
exitCode: proc.status,
signal: proc.signal ?? null,
provider: resolved.execution.provider,
+8
View File
@@ -0,0 +1,8 @@
{
"taskVersion": 1,
"id": "t-session-teach",
"prompt": "Remember this code word for later: mosaico. Reply with exactly: REMEMBERED",
"session": "demo",
"expectExact": "REMEMBERED",
"timeoutSeconds": 180
}
+7
View File
@@ -0,0 +1,7 @@
{
"taskVersion": 1,
"id": "t-session-recall",
"prompt": "What code word did I ask you to remember earlier in this session? Reply with only the code word.",
"session": "demo",
"timeoutSeconds": 180
}