/** * Sessions that live in this process, so the web client talks to an agent rather than to a * subprocess. * * The old /run spawned `node dist/index.js ` per request. That meant no state between * messages — every message was a fresh agent with no memory of the last one — no permission * prompts, because there was no terminal to ask at, and no structure, because the only channel was * stdout. Holding the session here gives all three: the conversation continues, a tool call can * wait for an answer from the browser, and every callback becomes an event. */ import type { AgentConfig } from '../types.js'; import { EventBus } from './events.js'; import { summarise as summariseChanges, type FileChange } from './changes.js'; import type { TrajectoryEvent } from '../trajectory.js'; import { type ModeName } from '../permissions.js'; import type { FailoverPolicy } from '../failover.js'; import type { Message } from '../types.js'; import type { Plan } from '../plan.js'; import type { Resumable } from './resume.js'; export interface WebSessionInfo { /** Which permission mode this session is running in. */ mode: ModeName; /** The standing objective, prepended to every message. */ goal: string | null; effort: string; /** Whether there is a previous message to send again. */ canRetry: boolean; /** The endpoint this session will actually send to, which is not always the provider's own. */ endpoint: string; /** What this session leaves for whoever picks the work up, or null before its first turn. */ handoff: string | null; id: string; cwd: string; title: string; provider: string; model: string; createdAt: number; turns: number; totalTokens: number; /** Messages the user sent, as against turns the agent took. */ messages: number; /** The transcript file this session is written to, which outlives the process. */ file: string; /** How many events this process can replay — 0 after a restart, however many turns there are. */ replayable: number; /** The transcript this session continues, or null. */ continues: string | null; busy: boolean; watchers: number; } /** What one turn changed on disk. */ export interface TurnChanges { turn: number; at: number; ms: number; changes: FileChange[]; totals: { files: number; added: number; removed: number; }; } export declare class WebSession { readonly id: string; private config; readonly bus: EventBus; readonly createdAt: number; /** * Where this session sits in the order they were made. * * The listing sorted on createdAt, and two sessions created in the same millisecond have the * same one — so "newest first" was arbitrary between them. Found by a test that made two in a * row, which is exactly what clicking New twice does. */ order: number; title: string; turns: number; totalTokens: number; busy: boolean; private agent; /** The turn in flight, so stop has something to act on. */ private stopping; /** Keys typed into this session, by provider. In memory, and never written anywhere. */ private readonly typed; /** * The name this session is filed under. * * The same timestamped shape the terminal uses, rather than the in-process id: the listing sorts * by name, so a browser session called "wmt61afo5-1" would land in an arbitrary place among a * week of dated ones. */ readonly fileId: string; /** The transcript this one continues, when it continues one. */ private resumedFrom; /** Readable by the registry, so a listing can hide the half that moved. */ get continues(): string | null; /** Where an agent's commands are written down, shared with the ones you run. */ jobs: import('./jobs.js').JobRunner | null; /** The recorded job for the command running right now, if it is a command. */ private agentJob; /** History to start from, when this session is a continuation rather than a beginning. */ private seed; private readonly pending; private askCounter; private toolCounter; /** Whether the provider is currently unreachable, so the state is announced once rather than per attempt. */ private offline; /** Start times by tool call, so a duration can be reported without the engine providing one. */ private readonly toolStarted; /** * How the workspace looked when this session began, and when the last turn ended. * * Two snapshots rather than one. The whole-session view needs the first — a sum of per-turn * diffs is not the same thing, because a file written in turn two and reverted in turn five is * two changes and no net change. The per-turn view needs the second. */ private baseline; private previous; private baselineState; /** What each turn changed, in order. */ readonly turnChanges: TurnChanges[]; /** * The permission mode, and what the user has already agreed to. * * The engine asks the interface about every tool call and does not decide itself, so an interface * that asks about all of them asks about reading a directory — which is what this did, and it made * every turn stop dead waiting for a click on something nobody needs to approve. The CLI has had * the answer all along in permissions.ts: a mode, a risk per tool, and rules the user has already * accepted. Same machinery here, same default. */ mode: ModeName; private readonly rules; /** * A standing objective, prepended to every message. * * The terminal has kept one for a while and puts it in front of each send, along with the mode's * own prefix and the effort — none of which the browser was doing, so a plan-mode session in the * web was not actually being told to plan. Same prefix here, built the same way. */ goal: string | null; effort: string; /** What this session leaves behind, as of its last turn. */ handoff: string | null; /** The last thing the user asked, so it can be asked again. */ private lastText; constructor(id: string, config: AgentConfig); info(): WebSessionInfo; /** * Answers a permission request. * * `always` records a rule, so agreeing to a tool once does not mean agreeing to it forty times — * which is the difference between a permission prompt and an obstacle. */ answer(askId: string, allowed: boolean, always?: boolean): boolean; /** Sets or clears the standing objective. Trimmed and capped, as the terminal does. */ setGoal(text: string | null): string | null; /** * Sets the pace, which now reaches the model as a parameter and not only as words. * * The prefix stayed — it is what tells a model without a reasoning setting how much care is * wanted — but a reasoning model takes `reasoning_effort`, so the value goes into the config the * next agent is built from. Rebuilding is the point: the parameter is chosen per request from * this config, so a stale agent would keep the old pace for the rest of the session. */ setEffort(effort: string): void; /** What the user last asked, for asking it again. */ get retryable(): string | null; /** * Drops the conversation back to before the last thing the user said. * * The same operation the terminal's /rewind performs, and the reason it is useful is the same: a * turn that went the wrong way is better removed than argued with. Returns how many messages * are left, or null when there is nothing to undo. */ rewind(): number | null; /** Changes the permission mode from the browser. */ setMode(mode: ModeName): void; /** Every request still waiting, so a reconnecting client can show them again. */ outstandingAsks(): string[]; /** * What happened and when, from the running agent. * * Empty until the first message, because the agent is not built until there is something for it * to do — asking for timings before anything has run is a fair question with a short answer. */ trajectory(): readonly TrajectoryEvent[]; /** * The diagnostic report, or an empty one before anything has run. * * Empty rather than absent for the same reason the timings are: asking for a report before the * first message is a fair question, and "nothing has happened yet" is a better answer than a 404 * from a session that plainly exists. */ /** * Token counts for this session, live from the running agent. * * The agent exposes stats as a property; this hands it out as a small, stable shape for /api/spend * so the browser can show cost and tokens the way the terminal's /cost and /tokens do. Zeroed * before the first message rather than absent — the count is a fair question with a short answer. */ stats(): { promptTokens: number; completionTokens: number; totalTokens: number; turns: number; }; report(): import('../report.js').Report; private ensureAgent; /** * Seeds this session from a transcript on disk. * * Set before the agent is built, because the history is a constructor argument: an agent cannot * be handed a past after it has already started without either replaying it as new input or * reaching inside it, and both are worse than saying it must be done first. */ resumeFrom(seed: { messages: Message[]; stats: Resumable['stats']; plan: Plan | null; title: string; from?: string; }): void; /** * Takes the opening snapshot. * * Called once, off the critical path, when the session is created: the walk is bounded but a * large workspace still takes a moment, and nothing should wait on a diff view to start working. */ captureBaseline(timeoutMs?: number): Promise; /** Everything this session has changed, against how the workspace looked when it began. */ changesSinceStart(): Promise<{ changes: FileChange[]; totals: ReturnType; late?: boolean; } | null>; /** Whether the opening snapshot never arrived, so the caller can say so plainly. */ baselineFailed(): boolean; /** * Runs one message, with any pictures attached to it. * * The paths are files the browser uploaded into a scratch directory, because the engine attaches * images by path — sending bytes through the conversation would mean a second encoding of * something already on disk. */ send(text: string, imagePaths?: readonly string[]): Promise; /** Snapshots the workspace and records the difference from the previous turn. */ /** * Writes this session where every other session lives. * * The same file, format and directory the terminal uses, so one list holds both and either can * read the other's work — a session started in the browser can be resumed at a terminal and the * other way round. It was in neither before: the browser kept everything in memory. */ private namedProperly; private improveTitle; /** Renames the session and tells whoever is watching, so the picker follows. */ private setTitle; persist(): Promise; private recordTurnChanges; /** Stops a run in progress, if the engine supports it. */ /** * Stops the turn in flight. * * Reported as "clicking stop is not working", and it never had: an agent session exposes no * abort method, so the optional call resolved to nothing and returned as though it had worked. * The signal handed to send() is the only thing that stops a turn, so that is what is held. */ abort(): boolean; /** * A key for this session, held in memory and written nowhere. * * The same bargain the terminal's /key makes, and deliberately not a stronger one: KONECK does * not store credentials, so neither does this. It reaches the provider on the next request and * disappears when the process does — and what makes it permanent is a line in a shell profile * fed from a password manager, not a file KONECK writes. * * Never emitted. The notice says a key was accepted and nothing about what it was. */ useKey(key: string, forProvider?: string, remember?: boolean): { ok: boolean; env: string; provider: string; remembered: boolean; file?: string; }; /** The key typed for a provider, for building a client with. Never serialised. */ typedKey(provider: string): string | undefined; /** * Which providers this session could reach, counting keys typed into it. * * The listing used to answer from `process.env` alone, so a key entered in the browser changed * nothing it said: the provider still read "no key set" and the field offering to fix it never * went away. The environment is not the only place a key can be by then. */ typedKeyFor(provider: string): boolean; /** * Sets the failover policy for this session, taking effect on the next message. * * A new agent, because the policy is read when the run starts. Changing it and having it apply * only to sessions started later is the kind of setting nobody trusts. */ setFailover(policy: FailoverPolicy): void; /** Changes the model or endpoint, which means a new agent on the next message. */ reconfigure(patch: Partial): void; /** The endpoint this session will actually use. Visible because it was not, and that cost a day. */ endpoint(): string; } /** Every live session, by id. */ export declare class SessionRegistry { private readonly sessions; private counter; /** The names live sessions are filed under, so a listing does not show them twice. */ fileIds(): string[]; /** Transcripts a live session is continuing, which should not also appear as finished. */ continuedFrom(): string[]; create(config: AgentConfig): WebSession; get(id: string): WebSession | undefined; list(): WebSessionInfo[]; remove(id: string): boolean; get size(): number; } /** A workspace is a directory; this is what a picker needs to show one. */ export declare function workspaceLabel(cwd: string): string; //# sourceMappingURL=sessions.d.ts.map