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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ Activity** and **Settings**.
From chat, `/ms login --backend https://api.main-sequence.app` does the same sign-in, and
`/ms auth-status --check` verifies it.

A sign-in lasts a fixed time that the backend sets (five days by default); renewing the
session does not extend it. From a day before it ends, the panel header and the account in
`/ms` results say **MainSequence session ends** with the local time and point to
`/ms login`. Once it has ended, they say when and ask you to sign in again. A refused
permission (HTTP 403) shows its message as an ordinary error: signing in again does not
change it.

## What you can do

- **Agents.** Browse the environment's agents with their descriptions, skills and example
Expand Down
17 changes: 15 additions & 2 deletions docs/agents-tools-panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ stay unchanged. No probe or custom Codex build is shipped.

| Surface | Implemented behavior |
| --- | --- |
| Header/navigation | MainSequence emblem, shared account, environment, Activity, Settings; Home/Code/Projects/Agents/Tools; compact rows and grouped details; host theme variables, neutral fallbacks and reduced-motion icons |
| Header/navigation | MainSequence emblem, shared account with when its login ends, environment, Activity, Settings; Home/Code/Projects/Agents/Tools; compact rows and grouped details; host theme variables, neutral fallbacks and reduced-motion icons |
| Agents | Search/pages, real icons, defaults, descriptions, skills/examples; Talk prepares a stable session without sending; new conversations; drafts survive browsing; direct messages and task mode |
| Native context/recovery | Immutable request saved before native submission, at-most-once dispatch, durable receipts, same-ID recovery, uncertain sends blocked; native MCP results and labelled replies remain conversation context |
| Agent tasks | Accepted task receipt, get/wait/cancel, continue an owned saved task, outputs in native context; no task read/recovery resends the original message |
Expand Down Expand Up @@ -63,7 +63,9 @@ known in another environment names that environment; an unknown release says whi
(`mainsequence_view connections` or `mainsequence_discover tools`) must come first; an
omitted `arguments` object on `mainsequence_call_tool` is an empty object; release ids match
regardless of case. `CONTEXT_CHANGED` carries its cause (`AUTH_REJECTED: The MCP endpoint
returned HTTP 401.`).
returned HTTP 401.`). Only `AUTH_REJECTED` and `NOT_AUTHENTICATED` end the view: `FORBIDDEN`
(HTTP 403, a permission the signed-in account lacks) keeps the context, and the panel, the
tools and `/ms` show its message as an ordinary error with no sign-in prompt.

Agent icons come inline from the platform's agent directory (`include_icons: true`); the
panel does not request individual icon URLs, and a missing or invalid image renders the
Expand Down Expand Up @@ -129,6 +131,17 @@ dispatch are never retried; after a process dies during send, or a transport res
ambiguous, the request is **uncertain** and resend is blocked. A new request must never be
used to bypass an uncertain one.

Login end: a saved login ends at a fixed moment, its refresh token's expiry, which
devtools reports as `sessionEndsAt` in `auth-status`; renewing does not move it.
`mainsequence_open` returns it with the account. From a day before, the header's account
dot turns amber and the header says "MainSequence session ends today at 14:30 — sign in
again (/ms login)", in local time. Once it has passed, devtools reports the account signed
out without contacting the backend, and the header shows **Session ended** with when it
ended. While the panel stays open, a timer redraws the header at the warning and at the end;
the next request then refuses the old view and asks for a refresh. The account in
`/ms login`, `/ms auth-status`, `/ms refresh-token` and `/ms status` results gives the same
warning and end, and otherwise names when the login ends.

Plugin version and update: the panel header always shows the running plugin version. When
the panel opens, the server compares it with the `mainsequence` catalog on this
repository's default branch and with the copy Codex has installed (`codex plugin list
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"check:docs": "node scripts/check-docs.cjs"
},
"dependencies": {
"@dev-mainsequence/devtools": "0.3.14"
"@dev-mainsequence/devtools": "0.3.15"
},
"devDependencies": {
"@types/node": "18.19.130",
Expand Down
26 changes: 25 additions & 1 deletion src/hosts/codexPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,37 @@ function fields(value: unknown, indent = ''): string[] {
return lines;
}

// A saved login ends at a fixed moment, its refresh token's expiry, which devtools reports as
// `sessionEndsAt`; renewing does not move it. Within a day of it the account warns, and once it
// has passed it says when the login ended. Times are the machine's local time.
const LOGIN_WARN_MS = 86_400_000;
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const two = (value: number): string => String(value).padStart(2, '0');
/** "today at 12:30", "tomorrow at 09:05", "yesterday at 18:00" or "on 30 Oct at 07:16". */
function loginWhen(at: number, now: number): string {
const date = new Date(at), day = (value: number) => new Date(value).toDateString();
const named = { [day(now)]: 'today', [day(now + 86_400_000)]: 'tomorrow', [day(now - 86_400_000)]: 'yesterday' }[day(at)];
return `${named || `on ${date.getDate()} ${MONTHS[date.getMonth()]}`} at ${two(date.getHours())}:${two(date.getMinutes())}`;
}
function loginEnd(session: Record<string, unknown>, now = Date.now()): { notice: string; line: string } {
const at = typeof session.sessionEndsAt === 'string' ? Date.parse(session.sessionEndsAt) : NaN;
if (!Number.isFinite(at)) return { notice: '', line: '' };
const when = loginWhen(at, now);
if (at <= now) return { notice: `MainSequence session ended ${when} — sign in again (/ms login)`, line: '' };
if (at - now <= LOGIN_WARN_MS) return { notice: `MainSequence session ends ${when} — sign in again (/ms login)`, line: '' };
return { notice: '', line: `Session ends ${when}` };
}

function account(value: unknown): string[] {
const session = record(value);
const session = record(value), end = loginEnd(session);
const username = typeof session.username === 'string' && session.username ? ` as ${singleLine(session.username)}` : '';
const lines = [session.authenticated ? `Signed in${username}` : 'Not signed in'];
if (end.notice) lines.push(end.notice);
if (session.backend) lines.push(`Backend: ${singleLine(session.backend)}`);
if (session.source === 'machine') lines.push('Login shared with your SDKs');
else if (session.source === 'environment') lines.push('Using the login from your environment');
else if (session.source === 'host') lines.push('Using the editor login');
if (end.line) lines.push(end.line);
if (session.expiresAt) lines.push(`Expires: ${singleLine(session.expiresAt)}`);
if (session.checked) lines.push('Verified with the backend');
return lines;
Expand All @@ -120,6 +143,7 @@ export function displayCodexResult(result: ActionResult): string {
sections.push(result.summary);
const code = result.error?.code;
if (code === 'INVALID_REQUEST') sections.push('For commands and examples: /ms --help');
// FORBIDDEN (HTTP 403) is a refused permission for a signed-in account, so it offers no sign-in.
else if (code === 'NOT_AUTHENTICATED' || code === 'AUTH_REJECTED') sections.push('Sign in again: /ms login');
} else if (result.action === 'repositories' && Array.isArray(result.data)) sections.push(repositories(result.data));
else if (['login', 'refresh-token', 'auth-status'].includes(result.action)) sections.push(account(result.data).join('\n'));
Expand Down
4 changes: 3 additions & 1 deletion src/hosts/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function success<T>(result: ActionResult<T>): T {
}
function account(value: SessionStatus) {
return { backend: text(value.backend), authenticated: value.authenticated === true, username: text(value.username, 300),
source: text(value.source, 40), checked: value.checked === true, expiresAt: text(value.expiresAt, 100) };
source: text(value.source, 40), checked: value.checked === true, expiresAt: text(value.expiresAt, 100), sessionEndsAt: text(value.sessionEndsAt, 100) };
}
const environmentLabel = (context: Context, uid: string) => `${context.environments.get(uid) || 'environment'} (${uid})`;
/** A release the context has seen, in this environment. Says where it was seen instead when it belongs elsewhere. */
Expand Down Expand Up @@ -91,6 +91,8 @@ export function createDiscovery(dependencies?: {
let answer;
try { answer = await run(context, environmentUid); }
catch (error) {
// A sign-in failure ends this view. FORBIDDEN (HTTP 403) is a refused permission for a signed-in
// account: it keeps the context and reaches the caller as an ordinary error.
if (error instanceof DiscoveryError && ['AUTH_REJECTED', 'NOT_AUTHENTICATED'].includes(error.code)) {
contexts.delete(String(args.contextId));
throw new DiscoveryError('CONTEXT_CHANGED', `The account or session changed (${error.code}: ${error.message}). Refresh MainSequence and retry once.`);
Expand Down
Loading
Loading