@primusclaw/personal-agent 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.js +7985 -6816
  2. package/dist/types/assistant/chat.d.ts +2 -2
  3. package/dist/types/assistant/events.d.ts +6 -1
  4. package/dist/types/capabilities/capability.d.ts +6 -0
  5. package/dist/types/capabilities/natural.d.ts +1 -1
  6. package/dist/types/capabilities/phrases.d.ts +5 -0
  7. package/dist/types/commands/names.d.ts +1 -1
  8. package/dist/types/config/settings.d.ts +6 -0
  9. package/dist/types/feedback/reporting.d.ts +10 -0
  10. package/dist/types/group-context/buffer.d.ts +66 -0
  11. package/dist/types/group-context/command.d.ts +11 -0
  12. package/dist/types/group-context/index.d.ts +3 -0
  13. package/dist/types/index.d.ts +2 -0
  14. package/dist/types/locales/assistant-chat.d.ts +4 -4
  15. package/dist/types/locales/connector-reasons.d.ts +1 -0
  16. package/dist/types/locales/group-context.d.ts +44 -0
  17. package/dist/types/locales/link.d.ts +1 -0
  18. package/dist/types/locales/remote-agents.d.ts +12 -1
  19. package/dist/types/locales/routing.d.ts +9 -2
  20. package/dist/types/locales/terminal-approvals.d.ts +44 -0
  21. package/dist/types/ports/claw/remote-agents.d.ts +4 -0
  22. package/dist/types/ports/store/group-context.d.ts +10 -0
  23. package/dist/types/ports/store/index.d.ts +3 -0
  24. package/dist/types/ports/store/link.d.ts +19 -0
  25. package/dist/types/ports/store/remote.d.ts +12 -1
  26. package/dist/types/proactive/deliver.d.ts +18 -1
  27. package/dist/types/proactive/deps.d.ts +6 -0
  28. package/dist/types/protocol/claw-api/assistant-events.d.ts +35 -1
  29. package/dist/types/protocol/remote-agents.d.ts +34 -0
  30. package/dist/types/remote/approvals-command.d.ts +8 -0
  31. package/dist/types/remote/chats.d.ts +40 -0
  32. package/dist/types/remote/events.d.ts +4 -1
  33. package/dist/types/remote/follow/control.d.ts +10 -0
  34. package/dist/types/remote/prompt.d.ts +14 -2
  35. package/dist/types/remote/state.d.ts +4 -1
  36. package/dist/types/remote/terminal-asks.d.ts +9 -0
  37. package/dist/types/remote/turn-state.d.ts +16 -1
  38. package/dist/types/remote/turns/chat-turn.d.ts +12 -1
  39. package/dist/types/remote/turns/display.d.ts +2 -0
  40. package/dist/types/remote/turns/runner.d.ts +15 -0
  41. package/dist/types/remote/turns/window-turn.d.ts +5 -1
  42. package/dist/types/remote-view/index.d.ts +3 -0
  43. package/dist/types/remote-view/terminal-ask-view.d.ts +68 -0
  44. package/dist/types/routing/commands-session.d.ts +3 -1
  45. package/dist/types/routing/credentials.d.ts +9 -0
  46. package/dist/types/routing/deps.d.ts +3 -1
  47. package/dist/types/routing/features.d.ts +14 -3
  48. package/dist/types/routing/group-router.d.ts +2 -0
  49. package/dist/types/routing/personal-router.d.ts +13 -0
  50. package/dist/types/routing/turn-registry.d.ts +23 -6
  51. package/dist/types/routing/turn-start.d.ts +13 -5
  52. package/dist/types/subscriptions/subscriptions.d.ts +6 -2
  53. package/package.json +1 -1
@@ -155,6 +155,40 @@ export interface RemoteSessionNew {
155
155
  takeover: boolean;
156
156
  }
157
157
  export declare function parseSessionNew(body: unknown): Parsed<RemoteSessionNew>;
158
+ /**
159
+ * Approvals a terminal asks for, carried to the phone (`POST /v1/remote-agents/connectors/:cid/terminal-sessions`).
160
+ *
161
+ * A coding agent its person started in their own terminal asks there before it runs a command, edits a file, leaves plan mode or
162
+ * puts a multiple-choice question. A connector whose hook sees such a prompt opens (or finds) one session per terminal conversation
163
+ * (`ref`, the agent's own conversation id) and posts each prompt on it as a `permissionRequest`, with
164
+ * `data.origin` {@link REMOTE_ORIGIN_TERMINAL_HOOK}; the owner answers it as any other, with a `decision`. The terminal keeps its own
165
+ * prompt: whichever answer comes first is the one the agent takes, and the connector says which (`permission_resolved.by`).
166
+ *
167
+ * Such a session carries nothing else: no prompt, no turn, no mode. It is not one of the sessions a user opens, so it neither counts
168
+ * against {@link REMOTE_AGENT_LIMITS.maxActiveSessionsPerUser} nor is closed to make room for one.
169
+ */
170
+ export declare const REMOTE_ORIGIN_TERMINAL_HOOK = "terminal_hook";
171
+ /** The only commands a terminal-hook session takes: an answer, and closing it. */
172
+ export declare const TERMINAL_HOOK_COMMANDS: readonly RemoteCommandType[];
173
+ export declare const TERMINAL_SESSION_LIMITS: {
174
+ /** Terminal-hook sessions one user may open per minute, across their machines (a new terminal conversation opens one). */
175
+ readonly opensPerMinute: 30;
176
+ /** Characters of a session's title (the conversation's name, or the start of its first prompt). */
177
+ readonly maxTitleChars: 80;
178
+ };
179
+ export interface RemoteTerminalSessionOpen {
180
+ agent_id: string;
181
+ /** The agent's own id of the conversation open in the terminal. */
182
+ ref: string;
183
+ cwd: string;
184
+ /** The conversation's name, or the start of its first prompt; empty when the connector has neither. */
185
+ title: string;
186
+ }
187
+ export declare function parseTerminalSessionOpen(body: unknown): Parsed<RemoteTerminalSessionOpen>;
188
+ /** The owner's switch for approvals from terminals (`GET`/`PUT /v1/remote-agents/approvals`). On unless they turned it off. */
189
+ export declare function parseApprovalsSetting(body: unknown): Parsed<{
190
+ enabled: boolean;
191
+ }>;
158
192
  /**
159
193
  * Hiding conversations of a machine from its owner's lists (`POST /v1/remote-agents/connectors/:cid/hidden`), and the
160
194
  * listing that leaves them out (`GET /v1/remote-agents/connectors`, `?hidden=include` to list them too). Nothing on the
@@ -0,0 +1,8 @@
1
+ import type { ClawRemoteAgentsPort } from "../ports/claw/remote-agents.js";
2
+ import type { CommandHandler, CommandRoute } from "../ports/routing.js";
3
+ export declare class TerminalApprovalsCommand implements CommandHandler {
4
+ private readonly claw;
5
+ readonly name = "approvals";
6
+ constructor(claw: Pick<ClawRemoteAgentsPort, "approvals" | "setApprovals">);
7
+ run(route: CommandRoute, args: string): Promise<boolean>;
8
+ }
@@ -20,6 +20,23 @@ export interface RemoteTurn {
20
20
  /** Nothing runs it any more (or nothing ever did): a Stop card's press for it stops nothing. */
21
21
  readonly ended: boolean;
22
22
  }
23
+ /**
24
+ * A message sent to the attached session while a turn of the chat still ran. Claw keeps it in order and the machine runs it once the
25
+ * turn in front has ended (the connector queues it, as Claude Code queues what is typed while it works); its own turn is read right
26
+ * after the one in front (`TurnRunner`).
27
+ */
28
+ export interface QueuedPrompt {
29
+ /** The channel's id of the person's message ("" none): a redelivered message queues nothing more. */
30
+ readonly messageId: string;
31
+ /** Claw's sequence number of the prompt (a refusal of it names it); undefined while its send is on its way, or none was given. */
32
+ readonly cmdSeq: number | undefined;
33
+ /** Start reading its turn: false when it cannot be (the chat is no longer attached to that session). */
34
+ start(): boolean;
35
+ }
36
+ /** At most this many messages wait behind a running turn (the connector refuses more: `too_many_queued_prompts`). */
37
+ export declare const QUEUED_PROMPTS_MAX = 5;
38
+ /** Refusals of prompts no queued message could be matched to yet, kept per chat for the send that is still on its way. */
39
+ export declare const REFUSALS_KEPT = 20;
23
40
  /** A turn about to be watched (its task is set right after). */
24
41
  export declare function newTurn(): RemoteTurn;
25
42
  /** The confirmation card for taking over a conversation a terminal has open: only its own Take over carries the ticket. */
@@ -37,6 +54,10 @@ export declare class ChatRegistry {
37
54
  /** Chats `forget` was called for: nothing follows them until they attach again. */
38
55
  readonly unlinked: Set<string>;
39
56
  readonly turns: Map<string, RemoteTurn>;
57
+ /** The messages sent while the chat's turn ran, oldest first: each one's turn is read after the one in front. */
58
+ private readonly queuedPrompts;
59
+ /** Prompt refusals (Claw's number -> the reason) that came before any queued message had that number. */
60
+ private readonly refusals;
40
61
  /** A short watch (`/agent new` waiting for the agent, `/agent mode` for its answer). */
41
62
  readonly watchers: Map<string, Task>;
42
63
  /** Whether the agent of the attached session starts in a mode that does not ask (its connector's report). */
@@ -48,6 +69,25 @@ export declare class ChatRegistry {
48
69
  private readonly readOnlyKnown;
49
70
  private readonly terminalInput;
50
71
  private readonly tickets;
72
+ /** How many messages wait behind the chat's running turn. */
73
+ queuedCount(chatKey: string): number;
74
+ enqueue(chatKey: string, prompt: QueuedPrompt): void;
75
+ /** The oldest message waiting, taken off the queue. */
76
+ takeQueued(chatKey: string): QueuedPrompt | undefined;
77
+ /** Whether a message with this channel id already waits in the chat's queue. */
78
+ queuedMessage(chatKey: string, messageId: string): boolean;
79
+ /** A prompt refusal no queued message has the number of yet (its send has not answered): kept for that send. */
80
+ noteRefusal(chatKey: string, cmdSeq: number, reason: string): void;
81
+ /** The reason the machine refused the prompt `cmdSeq` before its send answered, taken (undefined: not refused). */
82
+ takeRefusal(chatKey: string, cmdSeq: number): string | undefined;
83
+ /** Whether `prompt` still waits in the chat's queue. */
84
+ isQueued(chatKey: string, prompt: QueuedPrompt): boolean;
85
+ /** Take `prompt` off the queue (its send failed); false when it is not there (its turn was started meanwhile). */
86
+ removeQueued(chatKey: string, prompt: QueuedPrompt): boolean;
87
+ /** The waiting message Claw numbered `cmdSeq` (refused before it ran), taken off the queue. */
88
+ takeQueuedSeq(chatKey: string, cmdSeq: number): QueuedPrompt | undefined;
89
+ /** Every waiting message is given up (a stop drops them on the machine too); how many there were. */
90
+ dropQueued(chatKey: string): number;
51
91
  watching(chatKey: string): boolean;
52
92
  /** The chat's session only watches a conversation open in a terminal (read-only). */
53
93
  watches(chatKey: string, sessionId: string | null): boolean;
@@ -29,13 +29,16 @@ export interface RemoteStep {
29
29
  readonly started: boolean;
30
30
  /** The turn is over. */
31
31
  readonly finished: boolean;
32
+ /** Claw's number of the prompt whose turn ended (a `turn_end` that names it), null when it does not say. */
33
+ readonly endsSeq: number | null;
32
34
  readonly stopReason: string;
33
35
  /** The session ended: the reason, "" when none was given. */
34
36
  readonly closed: string | null;
35
- /** A command the connector refused. */
37
+ /** A command the connector refused (`cmdSeq`: Claw's number of it, null when the connector did not name it). */
36
38
  readonly rejected: {
37
39
  readonly type: string;
38
40
  readonly reason: string;
41
+ readonly cmdSeq: number | null;
39
42
  } | null;
40
43
  /** An event of history the agent replayed: never shown as new. */
41
44
  readonly replay: boolean;
@@ -24,10 +24,20 @@ export declare class FollowControl {
24
24
  drop(chatKey: string): void;
25
25
  /** Stop the reader before a message goes to the agent, with what it has read delivered: true when there was one to stop. */
26
26
  stop(chatKey: string): Promise<boolean>;
27
+ /**
28
+ * A conversation open in a terminal, watched or typed into: what is typed there is answered there, with no turn of the phone's to
29
+ * end it, so its reply is told after a few quiet seconds, as a watch's is. With the half-minute of a session of the phone's own it
30
+ * came after the person had left (2026-11-11 guide re-check: nothing within 20 s, although the card says replies show up here).
31
+ * Read each time they are used: a worker that restarted learns only after its reader started whether the session is a terminal's
32
+ * (`resume`).
33
+ */
34
+ private timingsOf;
27
35
  /** The chat leaves its session. */
28
36
  detach(chatKey: string): void;
29
37
  /** Follow every chat that is attached: a worker that starts delivers what its agents said while it was away, once. */
30
38
  resume(): void;
39
+ /** What this process forgot in a restart: whether the chat's session only watches or types into a terminal (its timings). */
40
+ private learn;
31
41
  shutdown(): void;
32
42
  /**
33
43
  * TEAMS_DIRECT_CHAT is on for this person: an attachment that still has the session card as its window is moved to the chat,
@@ -1,10 +1,10 @@
1
1
  import { type Conversation } from "../ports/surface.js";
2
2
  import type { StatusBoard } from "./board.js";
3
- import type { RemoteTurn } from "./chats.js";
4
3
  import type { RemoteContext } from "./context.js";
5
4
  import type { Provider } from "./ended.js";
6
5
  import type { FollowControl } from "./follow/control.js";
7
6
  import type { SessionStarts } from "./sessions.js";
7
+ import { type RemoteTurn } from "./chats.js";
8
8
  import { type ChatState } from "./state.js";
9
9
  import type { TurnRunner } from "./turns/runner.js";
10
10
  import type { Watches } from "./watches.js";
@@ -35,9 +35,19 @@ export declare class Prompts {
35
35
  private say;
36
36
  /** Plain text from a chat that is attached: it is the agent's, not the assistant's. */
37
37
  prompt(p: Prompt): Promise<void>;
38
+ /**
39
+ * A message while the chat's turn runs: sent to the session now (Claw keeps it in order; the machine runs it once the turn in
40
+ * front has ended), and its turn is read after that one. It is put in the queue before it is sent: the turn in front may end
41
+ * while the send is on its way, and its turn must be read from where that one ended, not by the follower. Refused only past what
42
+ * the machine queues.
43
+ */
44
+ private queueBehind;
38
45
  /** The terminal is using the session: what is typed here is answered, never sent. */
39
46
  private watched;
40
- /** The prompt to Claw; false (and the person told why) when it did not go. */
47
+ /**
48
+ * The prompt to Claw, with Claw's number of it (`duplicate`: Claw had this message already, a redelivery); false (and the person
49
+ * told why) when it did not go.
50
+ */
41
51
  private send;
42
52
  private failure;
43
53
  private started;
@@ -54,6 +64,8 @@ export declare class Prompts {
54
64
  stop(chatKey: string, key: string, to: Conversation, only?: RemoteTurn): Promise<boolean>;
55
65
  /** The Stop card's turn is still the one running for the chat, and the chat is still attached. */
56
66
  private stillOn;
67
+ /** What a stop says of the messages it gave up ("" none). */
68
+ private droppedLine;
57
69
  private cancel;
58
70
  /** A turn nobody reads any more is not open: what the agent says from here on is the follower's. */
59
71
  private closeTurn;
@@ -37,7 +37,10 @@ export declare class ChatState {
37
37
  /** The view when the chat has one, without making one. */
38
38
  existingView(chatKey: string): WindowView | undefined;
39
39
  forgetView(chatKey: string): void;
40
- /** The approvals the chat's session waits for, as the window shows them (none in a group: those go to the binder's 1:1). */
40
+ /**
41
+ * The approvals the chat's session waits for, as the window shows them (none in a group: those go to the binder's 1:1). A
42
+ * question a terminal asks is never among them: the window shows a tool and buttons, not the request, and it has its own card.
43
+ */
41
44
  approvalsOf(chatKey: string, state: RemoteState): RemotePermission[];
42
45
  /** The person's time zone (IANA), the default when theirs is not one. */
43
46
  zone(chatKey: string): string;
@@ -0,0 +1,9 @@
1
+ /** What is kept of a question a terminal asks (a connector's approval hooks), for the clicks on its card. */
2
+ import type { RemotePermission } from "../ports/store/remote.js";
3
+ import type { PermissionOption } from "./options.js";
4
+ /** The field of a kept terminal ask that says its card showed the request whole (set by this worker, never by Claw). */
5
+ export declare const SHOWN_WHOLE = "clawy_shown_whole";
6
+ /** Whether `perm` is a question a terminal asks. */
7
+ export declare function isTerminalPermission(perm: RemotePermission): boolean;
8
+ /** Whether `option` may be chosen for `perm`: a terminal's question not shown whole may only be turned down from here. */
9
+ export declare function mayChoose(perm: RemotePermission, option: PermissionOption): boolean;
@@ -8,6 +8,11 @@ import { type RemoteStep } from "./events.js";
8
8
  */
9
9
  export declare class RemoteTurnState {
10
10
  private readonly errors;
11
+ /**
12
+ * Claw's number of the prompt this turn answers, read at each event (a queued message's reader may start before its send has
13
+ * answered): the connector's refusal of it ends the turn (no `turn_end` follows one).
14
+ */
15
+ private readonly seqOf;
11
16
  /** Event ids already seen, oldest first (at most SEEN_KEPT). */
12
17
  readonly seen: Set<string>;
13
18
  /** Message text, one entry per stretch between tool calls (at most SEGMENTS_KEPT). */
@@ -17,8 +22,18 @@ export declare class RemoteTurnState {
17
22
  closed: string | null;
18
23
  note: string | null;
19
24
  private toolSinceText;
20
- constructor(errors: PublicErrors);
25
+ constructor(errors: PublicErrors,
26
+ /**
27
+ * Claw's number of the prompt this turn answers, read at each event (a queued message's reader may start before its send has
28
+ * answered): the connector's refusal of it ends the turn (no `turn_end` follows one).
29
+ */
30
+ seqOf?: () => number | undefined);
21
31
  feed(event: SseEvent): RemoteStep;
32
+ /** A turn end that names another prompt than this turn's. */
33
+ private othersEnd;
34
+ /** The connector refused the prompt this turn answers. */
35
+ private refusedOwn;
36
+ private end;
22
37
  /** The agent's last piece of text, stripped. */
23
38
  get answer(): string;
24
39
  /** The answer so far, and the text is forgotten (the event ids already seen are not): what a reader that goes on after a
@@ -19,7 +19,10 @@ export declare class RemoteStatusPolicy implements StatusPolicy<RemoteStep> {
19
19
  readonly view: LiveViewLike;
20
20
  constructor(live: RemoteLive, cutError: (text: string) => string, signer: Signer | null);
21
21
  applyStep(step: RemoteStep): boolean;
22
- /** Everything the agent has written this turn, as it is shown (under its head, with a signer). */
22
+ /**
23
+ * Everything the agent has written this turn, as it is shown (under its head, with a signer). `settled`: only what the rest of the
24
+ * text can no longer change, for the bubble while the agent still writes (`streamable`).
25
+ */
23
26
  private saidText;
24
27
  streamable(streamed: string): string;
25
28
  /**
@@ -29,6 +32,14 @@ export declare class RemoteStatusPolicy implements StatusPolicy<RemoteStep> {
29
32
  */
30
33
  finishStream(input: StreamFinishInput): StreamFinish | undefined;
31
34
  }
35
+ /**
36
+ * `text` up to a code span still being written: on its last line (where the agent is still writing; a span never crosses a line), the
37
+ * first run of backticks that no later run of the same length closes, and what follows it. Streamed, such a backtick is shown as a
38
+ * look-alike (`inertAnswer`: one the rich text cannot pair), and once the agent closes the span it is a backtick again: the text no
39
+ * longer begins with what the bubble shows, so the bubble ends on that lone character and the answer follows as a message of its own
40
+ * (2026-11-11 guide re-check: a reply that began with `` `hook.txt` `` ended its bubble on "ˋ"). Escaped backticks open nothing.
41
+ */
42
+ export declare function withoutOpenCode(text: string): string;
32
43
  /** The turn shown in the chat (a stream, else a progress message), as a `TurnDisplay`. */
33
44
  export declare class ChatTurn implements TurnDisplay {
34
45
  private readonly surface;
@@ -10,6 +10,8 @@ export interface TurnDisplay {
10
10
  close(hard?: boolean): void;
11
11
  /** The answer: `parts` in order, the suggested `replies` under the last. */
12
12
  finish(parts: readonly string[], options: FinishWith): Promise<void>;
13
+ /** After `finish`, when the turn of a queued message follows at once: what this turn showed must stay readable (optional). */
14
+ handOver?(): Promise<void>;
13
15
  }
14
16
  export interface FinishWith {
15
17
  /** What the agent wrote (the AI label). */
@@ -25,6 +25,8 @@ export interface TurnRequest {
25
25
  readonly state: RemoteState;
26
26
  /** The person's message the turn answers (a stream answers it); undefined: shown by a progress message. */
27
27
  readonly replyTo: string | undefined;
28
+ /** Claw's number of the prompt the turn answers, read as events come (undefined: not known, any turn end ends it). */
29
+ readonly cmdSeq?: (() => number | undefined) | undefined;
28
30
  }
29
31
  export declare class TurnRunner {
30
32
  private readonly ctx;
@@ -37,8 +39,16 @@ export declare class TurnRunner {
37
39
  constructor(ctx: RemoteContext, state: ChatState, board: StatusBoard, follow: FollowControl, ended: EndedWords, show: (chatKey: string, sid: string, request: Readonly<Record<string, unknown>>, to: Conversation) => Promise<void>, displayFor: DisplayFor);
38
40
  /** Watch the turn that is open on the chat's session. */
39
41
  start(request: TurnRequest): void;
42
+ /** Stop reading the chat's turn; the messages waiting behind it are given up too (nothing will read their turns). */
40
43
  cancel(chatKey: string): void;
44
+ /** Stop reading the chat's turn only (it will have none): the turn of the next queued message, if any, is read instead. */
45
+ stopReading(chatKey: string): void;
41
46
  private guarded;
47
+ /**
48
+ * The turn is over: the turn of the oldest message sent behind it is read next (it runs on the machine now), or, with none
49
+ * waiting, what the agent says next is followed from where the turn ended.
50
+ */
51
+ private over;
42
52
  private display;
43
53
  private run;
44
54
  /** The turn could not be read to its end: the session is gone, Claw refused, or the connection was lost. */
@@ -51,5 +61,10 @@ export declare class TurnRunner {
51
61
  /** One stream, read until it ends or the turn does. */
52
62
  private readStream;
53
63
  private apply;
64
+ /**
65
+ * A message waiting behind this turn was refused on the machine before it ran: it waits no more, and the person is told. One whose
66
+ * send has not answered yet has no number to match: the refusal is kept for that send (`Prompts.queueBehind`).
67
+ */
68
+ private queuedRefused;
54
69
  private sayRecovered;
55
70
  }
@@ -3,7 +3,7 @@
3
3
  * the card's output (an edit at most every EDIT_SECONDS), the step it runs and a message waiting for the terminal are its state,
4
4
  * and the end of the turn refreshes it (posted again at the bottom: a phone may not redraw an edit). Nothing else is posted.
5
5
  */
6
- import type { Conversation } from "../../ports/surface.js";
6
+ import { type Conversation } from "../../ports/surface.js";
7
7
  import { type RemoteStep } from "../events.js";
8
8
  import type { StatusBoard } from "../board.js";
9
9
  import type { ChatState } from "../state.js";
@@ -26,4 +26,8 @@ export declare class WindowTurn implements TurnDisplay {
26
26
  close(): void;
27
27
  /** (the window has no labels or replies: the options are the chat's) */
28
28
  finish(parts: readonly string[]): Promise<void>;
29
+ /** The turn of a queued message follows at once and takes the card over: this turn's answer is posted to the chat to stay. */
30
+ handOver(): Promise<void>;
29
31
  }
32
+ /** A finished answer the next queued turn takes the card from is posted to the chat up to this many characters. */
33
+ export declare const HANDED_OVER_MAX = 3500;
@@ -8,6 +8,7 @@ import type { DisconnectView, MachineChoiceView, ModeView, TakeoverView } from "
8
8
  import type { DecidedView, PermissionView, RemoteAskView } from "./permission-view.js";
9
9
  import type { SessionsView } from "./sessions-view.js";
10
10
  import type { StatusView } from "./status-view.js";
11
+ import type { TerminalAskView } from "./terminal-ask-view.js";
11
12
  import type { HiddenView } from "./hidden-view.js";
12
13
  import type { WindowCardView } from "./window-view.js";
13
14
  export * from "./agents-view.js";
@@ -18,6 +19,7 @@ export * from "./machine-views.js";
18
19
  export * from "./permission-view.js";
19
20
  export * from "./sessions-view.js";
20
21
  export * from "./status-view.js";
22
+ export * from "./terminal-ask-view.js";
21
23
  export * from "./window-view.js";
22
24
  export { remoteHealthText, remoteOffText, remoteQueuedText, remoteStopText, remoteText } from "../locales/remote.js";
23
25
  export { remoteWindowCardText } from "../locales/remote-window-card.js";
@@ -28,6 +30,7 @@ declare module "../ports/surface.js" {
28
30
  remoteStatus: StatusView;
29
31
  remotePermission: PermissionView;
30
32
  remoteAsk: RemoteAskView;
33
+ terminalAsk: TerminalAskView;
31
34
  remoteDecided: DecidedView;
32
35
  remoteDisconnect: DisconnectView;
33
36
  remoteTakeover: TakeoverView;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The card of a question a terminal asks: a coding agent its person started in their own terminal wants to run a command, change
3
+ * a file, start on a plan or have a multiple-choice question answered, and the connector's approval hooks put it here too
4
+ * (`remote.ask` with `origin: "terminal"`). The terminal keeps asking: the first answer counts.
5
+ *
6
+ * Nothing is approved unseen. The card shows the request whole (the connector and Claw carry it whole, credentials masked); a
7
+ * request whose content did not come (Claw leaves it out past its size), or that is longer than a card shows, keeps only the
8
+ * options that turn it down, and says to approve it at the terminal. So does one the card cannot show as it is: a credential
9
+ * masked in it (a mask can hide more than the credential), or a character the card does not show as itself.
10
+ */
11
+ import type { Lang } from "../locales/lang.js";
12
+ import type { DecisionButton } from "../remote/actions.js";
13
+ import { type PermissionOption } from "../remote/options.js";
14
+ import type { Reported } from "../remote/reported.js";
15
+ /**
16
+ * The most of a request's content a card shows, in characters. A Teams card carries about 28 KB; the content Claw sends is at
17
+ * most 16 KiB of JSON, and what is shown of it (unescaped, without the JSON's quoting) is no longer.
18
+ */
19
+ export declare const TERMINAL_CONTENT_MAX = 16000;
20
+ /** The most bytes of rendered content (as the card's JSON carries it) a card holds, leaving room under Teams' ~28 KB for the rest. */
21
+ export declare const TERMINAL_CARD_BYTES = 20000;
22
+ /** The content budget of the cut-short card a channel falls back to. */
23
+ export declare const TERMINAL_FALLBACK_BYTES = 4000;
24
+ /** One piece of a terminal request's content. `code`: shown as it is, in a fixed-width font; `words`: markdown kept, links not. */
25
+ export type TerminalSection = {
26
+ readonly type: "line";
27
+ readonly text: string;
28
+ } | {
29
+ readonly type: "code";
30
+ readonly text: string;
31
+ } | {
32
+ readonly type: "words";
33
+ readonly text: string;
34
+ } | {
35
+ readonly type: "option";
36
+ readonly label: string;
37
+ readonly description: string;
38
+ };
39
+ export interface TerminalAskView {
40
+ readonly lang: Lang;
41
+ readonly title: string;
42
+ /** Whose agent asks, on which machine, in which project. */
43
+ readonly who: string;
44
+ /** What it asks, before the content ("wants to run:"). */
45
+ readonly asks: string;
46
+ readonly sections: readonly TerminalSection[];
47
+ readonly notes: readonly string[];
48
+ readonly buttons: readonly DecisionButton[];
49
+ /** The whole request is on the card (else only the options that turn it down are). */
50
+ readonly complete: boolean;
51
+ /**
52
+ * The same card cut short (content within TERMINAL_FALLBACK_BYTES, only the options that turn it down, no "always" notes): what
53
+ * a channel sends when the card as built is larger than it can carry. Absent on the fallback itself.
54
+ */
55
+ readonly fallback?: TerminalAskView;
56
+ }
57
+ export interface TerminalAsk {
58
+ /** `remote.ask` as Claw relayed it: `action_id`, `tool`, `kind`, `input`, `question_index`, `question_count`. */
59
+ readonly ask: Reported;
60
+ readonly options: readonly PermissionOption[];
61
+ readonly sessionId: string;
62
+ readonly machine: string;
63
+ readonly agent: string;
64
+ }
65
+ /** Whether `remote.ask` is a question a terminal asks (a connector's approval hooks), not one an agent Claw started asks. */
66
+ export declare function isTerminalAsk(ask: Reported | undefined): boolean;
67
+ /** The card of a question a terminal asks. */
68
+ export declare function terminalAskView(input: TerminalAsk, lang?: Lang): TerminalAskView;
@@ -16,9 +16,11 @@ export declare class SessionCommands {
16
16
  /**
17
17
  * `/stop` of the turn on the chat's own session (not a remote agent's, not the assistant's). `only`: stop only while that
18
18
  * turn is the one running (a Stop card's turn), checked with no wait before the interrupt goes; false when it is not, and
19
- * nothing was said or stopped.
19
+ * nothing was said or stopped. `/stop` (no `only`) also gives up the messages queued behind the running turn, and says so.
20
20
  */
21
21
  stopTurn(chat: Chat, key: string, scope?: string, only?: ActiveTurn): Promise<boolean>;
22
+ /** The chat's turn Claw runs now: the first one not stopped already (one queued behind a stopped turn goes at once). */
23
+ private running;
22
24
  private interrupt;
23
25
  /**
24
26
  * The five sessions this chat used last, a card each with Show here. No session is shown on the worker's word alone: each
@@ -25,6 +25,15 @@ export declare class Credentials {
25
25
  get onExchange(): boolean;
26
26
  /** What to call Claw with for this person, or undefined after they were told what to do (`ask`). */
27
27
  credential(chat: Chat, ask?: boolean): Promise<string | undefined>;
28
+ /**
29
+ * After an answer of the assistant that was refused for want of a confirmed identity (`needs_identity`: a person whose chats
30
+ * run on their key because they never confirmed it asked for something only they may do, such as a review as them): the card
31
+ * that confirms it, unless the person has confirmed by now. Nothing when chats do not run on the exchange, or it cannot be
32
+ * asked. Never before that: connecting a machine and the rest of what a key may do are not held behind it.
33
+ */
34
+ identityAfterRefusal(chat: Chat): Promise<boolean>;
35
+ /** The binding card with `lead`; false (nothing sent) when the identity service gives no link: a key's owner is not sent the `/link` steps. */
36
+ private sendIdentityCard;
28
37
  /** A fresh credential per call when chats run on exchanged tokens (they expire); undefined: the key as it is. */
29
38
  provider(userId: string, key: string): CredentialProvider;
30
39
  /** The person's exchanged token, else `fallback` (a turn can outlive a 10-minute token: each Claw call asks again). */
@@ -13,7 +13,7 @@ import type { StorePort } from "../ports/store/index.js";
13
13
  import type { ConversationRef, OutMessage, Surface } from "../ports/surface.js";
14
14
  import type { JsonObject } from "../json.js";
15
15
  import type { SecurityEventName } from "../observability/security-events.js";
16
- import type { AssistantChat, ChannelContextBuilder, ConnectionsText, GithubChat, MessageText, NoteFactory, Observing, RemoteChat, ReviewStopReader, StopCards, Subscriptions } from "./features.js";
16
+ import type { AssistantChat, ChannelContextBuilder, ConnectionsText, GithubChat, GroupHistory, MessageText, NoteFactory, Observing, RemoteChat, ReviewStopReader, StopCards, Subscriptions } from "./features.js";
17
17
  import type { TurnRunner } from "./turn-runner.js";
18
18
  /** What a deployment sets for the router (the Python `Agent.__init__` arguments that are settings). */
19
19
  export interface RouterConfig {
@@ -97,6 +97,8 @@ export interface RouterDeps {
97
97
  readonly stopCards: StopCards;
98
98
  readonly proactive: ProactiveHold;
99
99
  readonly observing: Observing;
100
+ /** The recent messages of group chats, as background when the bot is @mentioned there. */
101
+ readonly groupHistory: GroupHistory;
100
102
  readonly github: GithubChat | undefined;
101
103
  /** The review-stop words of a group (with the review service on). */
102
104
  readonly reviewStop: ReviewStopReader | undefined;
@@ -52,9 +52,9 @@ export interface AssistantChat {
52
52
  board(userId: string, credential: string, conversation: Conversation): Promise<void>;
53
53
  /**
54
54
  * What the assistant is doing this moment is stopped. `onlyMessage`: only while Claw's running turn answers that
55
- * message; false, with nothing said or stopped, when it does not.
55
+ * message; false, with nothing said or stopped, when it does not. What is said is in `lang` (the chat's).
56
56
  */
57
- stop(userId: string, credential: string, conversation: Conversation, onlyMessage?: string): Promise<boolean>;
57
+ stop(userId: string, credential: string, conversation: Conversation, onlyMessage?: string, lang?: Lang): Promise<boolean>;
58
58
  /** Forget what is kept for the person (they unlinked). */
59
59
  forget(userId: string): void;
60
60
  /** True when the assistant took the message. */
@@ -134,7 +134,8 @@ export interface RemoteChat {
134
134
  }
135
135
  /** The person's notification subscription in Claw (Python `subscriptions.Subscriptions`); undefined when notifications are off. */
136
136
  export interface Subscriptions {
137
- ensure(userId: string): Promise<void>;
137
+ /** Made or mended with `credential` when given (a person with no link: their exchanged token), else the linked key. */
138
+ ensure(userId: string, credential?: string): Promise<void>;
138
139
  /** Before the key is revoked: Claw needs it to delete the subscription. */
139
140
  remove(userId: string, key: string): Promise<void>;
140
141
  }
@@ -146,6 +147,16 @@ export interface Observing {
146
147
  /** The suggestions the observed groups make for the person (`/board`). */
147
148
  board(userId: string, credential: string, conversation: Conversation): Promise<void>;
148
149
  }
150
+ /**
151
+ * The recent messages of a group chat, as background when the bot is @mentioned there (`group-context/buffer.ts`). Fed
152
+ * only by group messages that do not address the bot, which a channel delivers only where the chat allowed it.
153
+ */
154
+ export interface GroupHistory {
155
+ /** A group message not addressed to the bot: kept in memory, never answered. */
156
+ record(message: InboundMessage): void;
157
+ /** The chat's kept messages as background for a turn ("" none). */
158
+ background(chatId: string): string;
159
+ }
149
160
  /** The review service's part in the chat (Python `github_review.GithubReview` and `github_connect.GithubConnect`). */
150
161
  export interface GithubChat {
151
162
  /** Which channel id is whose, for reading a mention in a group. */
@@ -29,6 +29,8 @@ export declare class GroupRouter {
29
29
  /** A command of a 1:1 chat (`/agent attach|detach` binds the group: it stays the group's). */
30
30
  private oneToOneOnly;
31
31
  private withKey;
32
+ /** The chat's recent messages, as untrusted background, before what the person quoted (either may be ""). */
33
+ private withHistory;
32
34
  /** Which account, in a group: never the sender's email. */
33
35
  private whoami;
34
36
  /**
@@ -15,8 +15,21 @@ export declare class PersonalRouter {
15
15
  private readonly text;
16
16
  private readonly stopPress;
17
17
  private readonly showHere;
18
+ /** A person's bound id is read and kept one message at a time, so a late lookup of an earlier token never wins. */
19
+ private readonly boundLock;
18
20
  constructor(ctx: RouterContext, sets: CommandSets, text: PersonalText);
19
21
  route(event: InboundMessage | InboundAction, chat: Chat): Promise<void>;
22
+ /**
23
+ * A person with no link whose chats run on an exchanged token (bound at the identity service): their Claw user id is
24
+ * read with the token and their subscription made with it, as a link's are, so their 1:1 chat reaches the assistant
25
+ * from this message on (the assistant reaches only a person whose Claw user id and subscription it knows).
26
+ *
27
+ * The id is read again whenever the token is not the one it was read with (tokens last minutes, so at most once a token):
28
+ * a person who rebinds to another account is followed to it, and one no longer bound is forgotten, so the assistant
29
+ * never keeps answering for an account the person no longer holds (nor for one whose owner a new token cannot tell yet).
30
+ * A subscription is made or mended only with a token whose owner is the id kept.
31
+ */
32
+ private reachBound;
20
33
  /** Linked before the Claw user id was kept: it is read now. */
21
34
  private backfill;
22
35
  private message;
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * The turns this worker follows, per chat (the Python `Agent._turns`, `ActiveTurn`, `_queue_behind`, `_drop_turn`,
3
- * `_item_stopped`). A chat has the turn Claw runs and at most one queued behind it; a review for a colleague is kept
4
- * under a key of its own per pull request, so it neither waits for nor blocks the asker's chat turn.
3
+ * `_item_stopped`). A chat has the turn Claw runs and up to `QUEUED_TURNS_MAX` queued behind it, in the order they were sent; a
4
+ * review for a colleague is kept under a key of its own per pull request, so it neither waits for nor blocks the asker's chat turn.
5
5
  */
6
6
  import type { Clock } from "../ports/clock.js";
7
+ /** The messages that may wait behind a chat's running turn (as many as an attached agent's chat queues). */
8
+ export declare const QUEUED_TURNS_MAX = 5;
7
9
  /** A signal that is set once: what a message queued behind a turn waits for. */
8
10
  export declare class Latch {
9
11
  private done;
10
12
  private readonly waiters;
11
13
  get isSet(): boolean;
12
14
  set(): void;
15
+ /** Settles once it is set. */
16
+ whenSet(): Promise<void>;
13
17
  /** True when it was set within `ms` of `clock`, false when the wait ran out. */
14
18
  wait(ms: number, clock: Clock): Promise<boolean>;
15
19
  }
@@ -23,8 +27,17 @@ export declare class ActiveTurn {
23
27
  toolDenied: boolean;
24
28
  /** Claw took this turn's stop (its item's, or the interrupt): its failed end is the stop, not an error. */
25
29
  stopConfirmed: boolean;
30
+ /**
31
+ * Set once Claw took the interrupt of this turn (`/stop`): a message queued behind it need not wait for its end, which a turn
32
+ * stopped before Claw ran it may never report.
33
+ */
34
+ readonly stopped: Latch;
35
+ /** The turn went to Claw (a queued one once the turns in front of it were over or stopped): `/stop` no longer withdraws it. */
36
+ launched: boolean;
26
37
  /** Set once Claw runs the turn's message, or the turn is over: a message sent behind it may go then. */
27
38
  readonly running: Latch;
39
+ /** Set once the turn is over (or never started, or was given up while it waited): the turn behind it may go then. */
40
+ readonly ended: Latch;
28
41
  /** The turn is over (or never started). */
29
42
  over: boolean;
30
43
  constructor(
@@ -40,21 +53,25 @@ export interface TurnKey {
40
53
  /** For a review: "review:<item id>" or "review:<pull request>"; "" for a chat turn. */
41
54
  readonly review: string;
42
55
  }
43
- export type QueueRefusal = "still_working" | "one_waiting";
56
+ export type QueueRefusal = "still_working" | "queue_full";
44
57
  export declare class TurnRegistry {
45
58
  private readonly turns;
46
- /** The chat's turns, oldest first: the one Claw runs, then at most one queued behind it. */
59
+ /** The chat's turns, oldest first: the one Claw runs, then those queued behind it. */
47
60
  of(key: TurnKey): readonly ActiveTurn[];
48
61
  has(key: TurnKey): boolean;
49
62
  /** Registers `turn` as the first of a chat that has none; false when the chat has one. */
50
63
  claim(key: TurnKey, turn: ActiveTurn): boolean;
51
64
  /**
52
- * `turn` behind the chat's running one: refused when a review is either of them, or when one already waits. Otherwise
53
- * it is registered, and the caller waits for the front turn to run (`front`).
65
+ * `turn` behind the chat's running one: refused when a review is either of them, or when `QUEUED_TURNS_MAX` already wait (not
66
+ * gone to Claw yet). Otherwise it is registered last, with its place among those that wait (`waiting`: 1 is the next to go;
67
+ * 0 when it is the chat's first turn and runs at once).
54
68
  */
55
69
  enqueue(key: TurnKey, turn: ActiveTurn): {
56
70
  readonly front: ActiveTurn;
71
+ readonly waiting: number;
57
72
  } | QueueRefusal;
73
+ /** Every turn queued behind the chat's running one that has not gone to Claw yet is given up (`/stop`); how many there were. */
74
+ withdrawQueued(key: TurnKey): number;
58
75
  /** The turn is over (or never started): whatever waits behind it may go, and the chat no longer holds it. */
59
76
  drop(key: TurnKey, turn: ActiveTurn): void;
60
77
  /** A work item was stopped from its card: the review turn that follows it ends as stopped, not as a task that failed. */