Skip to content
Open
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
57 changes: 57 additions & 0 deletions docs/session-profile-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Session profile record format

`lib/session-profile-persistence.ts` defines the optional session profile v1
codec and pure replay contract. Adding this module does not change Enter,
startup, routing, shared defaults, or live orchestrator behavior.

## Payloads

A custom entry uses `customType: "gentle-pi.session-profile/v1"` and one payload:

```json
{"kind":"bind","origin":"user","name":"work","modelProfiles":{"worker":{"model":"provider/model"},"orchestrator":{"thinking":"high"}}}
{"kind":"clear"}
```

The encoder creates only explicit `user` selections. The decoder accepts the
closed origin set `user`, `local`, `repo`, and `global`; this does not implement
inherited startup persistence or follow mode. A bound empty snapshot is not a
clear. Snapshots are detached, known invalid fields reject the whole binding,
and unknown extra fields carry no routing meaning. Existing route normalization
supports legacy model strings and `effort`; valid `thinking` takes precedence.

## Replay

`replaySessionProfileBranch` requires entries already corroborated on disk,
ordered oldest to newest on the active branch. Supplying `getBranch()` alone
does not establish persistence. The newest profile-family entry is terminal:

| Result | Meaning |
| --- | --- |
| `absent` | No profile-family entry on the supplied branch. |
| `bound` | A detached, validated binding, including an empty snapshot. |
| `cleared` | An explicit clear; fallback is a later consumer's decision. |
| `invalid` | Invalid v1 data; never resurrect an earlier binding. |
| `unsupported` | Unknown profile-family identifier, regardless of payload. |

Replay neither appends nor publishes bindings and never applies a model or
thinking level. A subsequent disk-reader slice supplies corroboration; this
codec alone cannot establish it.

## Disk corroboration

`readSessionProfileDisk(source)` in `lib/session-profile-disk-reader.ts` is the
stateless synchronous check that supplies the corroboration replay requires.
It reads the session's public active branch and its JSONL file, selects the
newest profile-family entry on the branch (excluding caller-supplied known
failed append IDs), and admits it only when the record on disk is identical.
It performs no writes, fallback policy, caching, ancestry repair or fsync, and
a missing file is conservative: it never restores a profile found only in
memory.

The result is the decoder result plus `entryIndex` and `lineNumber`, or
`indeterminate` with one reason: `missing-source`, `invalid-candidate-metadata`,
`unreadable-file`, `source-changed`, `invalid-json`, `invalid-record`,
`session-header-mismatch`, `duplicate-id`, `missing-header`,
`selected-record-missing`, `selected-record-mismatch`, or
`unserializable-source`. Reasons never carry record contents.
7 changes: 6 additions & 1 deletion lib/model-routing-authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export type ModelConfigFileResult =
export const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
const SAFE_AGENT_NAME_PATTERN = /^[A-Za-z0-9._:@/+%-]+$/;

/** Shared agent-name policy; the same test normalizeModelConfig applies per key. */
export function isSafeAgentName(name: string): boolean {
return SAFE_AGENT_NAME_PATTERN.test(name);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Expand Down Expand Up @@ -78,7 +83,7 @@ export function normalizeModelConfig(value: unknown): AgentModelConfig | undefin
if (!isRecord(value)) return undefined;
const cleaned: AgentModelConfig = {};
for (const [name, entryValue] of Object.entries(value)) {
if (!SAFE_AGENT_NAME_PATTERN.test(name)) continue;
if (!isSafeAgentName(name)) continue;
const entry = normalizeRoutingEntry(entryValue);
if (entry) cleaned[name] = entry;
}
Expand Down
Loading