Official

chat

Built-in chat UI (JavaScript UI plugin).

The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/chat@0.2.0

Permissions in 0.2.0

  • Control chats chats.controlHighCreates chats, sends prompts and cancels runs.Send prompts, answer agent questions, and create, change, fork, revert, stop and group chats
  • Run other plugins' commands commands.runHighRuns commands of other plugins.Run the sidebar's New chat command from the title bar
  • Read chats chats.readMediumReads your transcripts and chat lists.Show workspaces, tasks, chats and transcripts, and follow them as agents work
  • Read files fs.readMediumReads files in the listed places.Suggest workspace files for @ mentionsPlaces: the open workspace
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the chat grid and the title barSlots: centertitle

Files

scheduler-policy.ts7.6 KB
// Stock release decisions over public snapshots. Delivery integrity stays in the
// host; this versioned, opaque state can be replaced by another scheduler.
import * as z from "zod";
import type { InputSnapshot, PendingInput, UsageRecovery } from "convergence";

export const recoveryWait = z.object({
  runId: z.string(),
  stoppedAt: z.string(),
  identity: z.string().optional(),
  scope: z.string().optional(),
  resetAt: z.string().optional(),
  reason: z.string(),
  status: z.enum(["armed", "paused", "attempting", "failed"]),
  attemptKey: z.string().optional(),
  inputId: z.string().optional(),
  generated: z.boolean().optional(),
  checkedKey: z.string().optional(),
});
export const policySchema = z.object({
  version: z.literal(1),
  generation: z.number(),
  handledRun: z.string().optional(),
  releasedRun: z.string().optional(),
  pause: z.string().optional(),
  wait: recoveryWait.optional(),
});
export type PolicyState = z.infer<typeof policySchema>;
export type RecoveryWait = z.infer<typeof recoveryWait>;
export const preferencesSchema = z.object({
  autoSendAfterLimit: z.record(z.string(), z.boolean()).default({}),
  generateContinue: z.boolean().default(false),
});
export type Preferences = z.infer<typeof preferencesSchema>;
export const defaults: Preferences = { autoSendAfterLimit: {}, generateContinue: false };
export const autoSend = (preferences: Preferences, agentId: string) => preferences.autoSendAfterLimit[agentId] !== false;
export const editable = (input: PendingInput) => input.state === "held" || input.state === "rejected";
export const uncertain = (snapshot: InputSnapshot) =>
  snapshot.inputs.some((input) => ["dispatching", "delivery_unknown", "native_admitted"].includes(input.state));
export const pending = (snapshot: InputSnapshot) => snapshot.inputs.filter((input) => input.state !== "observed_consumed");
export const firstQueued = (snapshot: InputSnapshot) =>
  snapshot.inputs.find((input) => input.intent === "queue" && input.state === "held" && !input.pauseReason);

export function policyOf(snapshot: InputSnapshot): PolicyState {
  const result = policySchema.safeParse(snapshot.policyState);
  if (result.success && result.data.generation === snapshot.generation) return result.data;
  // A changed attachment/generation is an invalidation seam, not a handoff
  // implementation. Neither a previous success nor a source timer can release it.
  return {
    version: 1,
    generation: snapshot.generation,
    handledRun: snapshot.lastRun?.id,
    releasedRun: snapshot.lastRun?.id,
    ...(snapshot.policyState != null ? { pause: "Attachment changed; send explicitly to resume" } : {}),
  };
}

export function settlePolicy(snapshot: InputSnapshot, state: PolicyState): PolicyState {
  const run = snapshot.lastRun;
  if (!run || run.generation !== snapshot.generation || run.id === state.handledRun) return state;
  const next = { ...state, handledRun: run.id };
  // Simulated cancellation is owned by the host reservation, never a FIFO release.
  if (run.steering) return next;
  if (run.outcome.status === "completed") return { ...next, wait: undefined, releasedRun: firstQueued(snapshot) ? state.releasedRun : run.id };
  if (run.outcome.status === "failed" && run.usageLimit && !state.pause) {
    const evidence = run.usageLimit;
    return {
      ...next,
      wait: {
        runId: run.id,
        stoppedAt: run.stoppedAt,
        identity: evidence.identity,
        scope: evidence.scope,
        resetAt: validReset(evidence.resetsAt, run.stoppedAt) ? evidence.resetsAt : undefined,
        reason: evidence.reason,
        status: "armed",
      },
    };
  }
  return { ...next, pause: state.pause ?? (run.outcome.status === "cancelled" ? "Stopped by you" : "Run failed"), wait: state.wait ? { ...state.wait, status: "paused" } : undefined };
}

export function fifoCandidate(snapshot: InputSnapshot, state: PolicyState): PendingInput | undefined {
  const run = snapshot.lastRun;
  if (snapshot.running || snapshot.settling || uncertain(snapshot) || state.pause || state.wait || !run || run.steering ||
      run.generation !== snapshot.generation || run.outcome.status !== "completed" || state.releasedRun === run.id) return;
  return firstQueued(snapshot);
}

export function validReset(reset: string | undefined, stoppedAt: string): boolean {
  return !!reset && Number.isFinite(Date.parse(reset)) && Date.parse(reset) > Date.parse(stoppedAt);
}
export function scopeOf(snapshot: InputSnapshot, wait: RecoveryWait): string | undefined {
  // Identity is never fabricated from a percentage or agent name. With no
  // pool reported, conservatively coordinate only that agent/credential pair.
  return wait.identity ? JSON.stringify([snapshot.agentId, wait.identity, wait.scope ?? "credential"]) : undefined;
}
export function waitKey(wait: RecoveryWait): string {
  return JSON.stringify([wait.runId, wait.identity, wait.scope, wait.resetAt]);
}
export function matching(wait: RecoveryWait, evidence: UsageRecovery): boolean {
  return !!wait.identity && evidence.identity === wait.identity && evidence.scope === wait.scope;
}
export function fresh(evidence: UsageRecovery, wait: RecoveryWait, checkedAt: number): boolean {
  const observed = Date.parse(evidence.observedAt);
  return observed >= Date.parse(wait.stoppedAt) && observed >= checkedAt - 5000 && observed <= checkedAt + 5000;
}
export type RecoveryDecision = { kind: "send"; speculative: boolean } | { kind: "hold"; reason: string; resetAt?: string } | { kind: "invalidate"; reason: string };
export function recoveryDecision(wait: RecoveryWait, evidence: UsageRecovery | undefined, now: number, restored: boolean): RecoveryDecision {
  if (!wait.identity) return { kind: "hold", reason: "Account identity unavailable; retry manually" };
  if (evidence?.identity && !matching(wait, evidence)) return { kind: "invalidate", reason: "Account or quota scope changed; retry manually" };
  const usable = evidence && matching(wait, evidence) && fresh(evidence, wait, now) ? evidence : undefined;
  if (restored && !usable) return { kind: "hold", reason: "Account could not be reconciled after restart; retry manually" };
  if (usable?.availability === "blocked") return { kind: "hold", reason: usable.reason, resetAt: validReset(usable.resetsAt, wait.stoppedAt) ? usable.resetsAt : undefined };
  if (wait.attemptKey === waitKey(wait)) return { kind: "hold", reason: "Recovery retry already attempted; retry manually" };
  if (usable?.availability === "allowed") return { kind: "send", speculative: false };
  if (validReset(wait.resetAt, wait.stoppedAt) && Date.parse(wait.resetAt ?? "") <= now) return { kind: "send", speculative: true };
  return { kind: "hold", reason: "Reset time unavailable; retry manually" };
}

export function orderedWaits(snapshots: readonly InputSnapshot[]): InputSnapshot[] {
  return snapshots.filter((snapshot) => policyOf(snapshot).wait?.status === "armed")
    .sort((a, b) => {
      const left = policyOf(a).wait, right = policyOf(b).wait;
      return (left?.stoppedAt ?? "").localeCompare(right?.stoppedAt ?? "") || a.chatId.localeCompare(b.chatId) ||
        (pending(a)[0]?.id ?? "").localeCompare(pending(b)[0]?.id ?? "");
    });
}

export function inputLabel(input: PendingInput): string {
  const prefix = input.origin === "generated_continue" ? "Generated Continue" : input.intent === "steer" ? "Steer" : input.intent === "send" ? "Send" : "Queued";
  const delivery = {
    held: "awaiting delivery", rejected: "not admitted", dispatching: "submitting", delivery_unknown: "delivery unknown — do not replay",
    native_admitted: "admitted — pickup not yet observed", observed_consumed: "pickup observed",
  }[input.state];
  return `${prefix} · ${delivery}${input.pauseReason ? ` · ${input.pauseReason}` : ""}`;
}

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3105.1 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.