@primusclaw/personal-agent 0.1.0 → 0.2.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 (47) hide show
  1. package/dist/index.js +7874 -6791
  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/remote-agents.d.ts +12 -1
  18. package/dist/types/locales/routing.d.ts +9 -2
  19. package/dist/types/locales/terminal-approvals.d.ts +44 -0
  20. package/dist/types/ports/claw/remote-agents.d.ts +4 -0
  21. package/dist/types/ports/store/group-context.d.ts +10 -0
  22. package/dist/types/ports/store/index.d.ts +3 -0
  23. package/dist/types/ports/store/remote.d.ts +12 -1
  24. package/dist/types/proactive/deliver.d.ts +11 -0
  25. package/dist/types/protocol/claw-api/assistant-events.d.ts +30 -1
  26. package/dist/types/protocol/remote-agents.d.ts +34 -0
  27. package/dist/types/remote/approvals-command.d.ts +8 -0
  28. package/dist/types/remote/chats.d.ts +40 -0
  29. package/dist/types/remote/events.d.ts +4 -1
  30. package/dist/types/remote/follow/control.d.ts +10 -0
  31. package/dist/types/remote/prompt.d.ts +14 -2
  32. package/dist/types/remote/state.d.ts +4 -1
  33. package/dist/types/remote/terminal-asks.d.ts +9 -0
  34. package/dist/types/remote/turn-state.d.ts +16 -1
  35. package/dist/types/remote/turns/chat-turn.d.ts +12 -1
  36. package/dist/types/remote/turns/display.d.ts +2 -0
  37. package/dist/types/remote/turns/runner.d.ts +15 -0
  38. package/dist/types/remote/turns/window-turn.d.ts +5 -1
  39. package/dist/types/remote-view/index.d.ts +3 -0
  40. package/dist/types/remote-view/terminal-ask-view.d.ts +68 -0
  41. package/dist/types/routing/commands-session.d.ts +3 -1
  42. package/dist/types/routing/deps.d.ts +3 -1
  43. package/dist/types/routing/features.d.ts +12 -2
  44. package/dist/types/routing/group-router.d.ts +2 -0
  45. package/dist/types/routing/turn-registry.d.ts +23 -6
  46. package/dist/types/routing/turn-start.d.ts +13 -5
  47. package/package.json +1 -1
@@ -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
@@ -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. */
@@ -146,6 +146,16 @@ export interface Observing {
146
146
  /** The suggestions the observed groups make for the person (`/board`). */
147
147
  board(userId: string, credential: string, conversation: Conversation): Promise<void>;
148
148
  }
149
+ /**
150
+ * The recent messages of a group chat, as background when the bot is @mentioned there (`group-context/buffer.ts`). Fed
151
+ * only by group messages that do not address the bot, which a channel delivers only where the chat allowed it.
152
+ */
153
+ export interface GroupHistory {
154
+ /** A group message not addressed to the bot: kept in memory, never answered. */
155
+ record(message: InboundMessage): void;
156
+ /** The chat's kept messages as background for a turn ("" none). */
157
+ background(chatId: string): string;
158
+ }
149
159
  /** The review service's part in the chat (Python `github_review.GithubReview` and `github_connect.GithubConnect`). */
150
160
  export interface GithubChat {
151
161
  /** 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
  /**
@@ -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. */
@@ -3,8 +3,6 @@ import type { InboundFile } from "../ports/surface.js";
3
3
  import type { Chat } from "./chat.js";
4
4
  import type { RouterContext } from "./context.js";
5
5
  import type { ReviewRunFields, TurnOutcome } from "./turn-runner.js";
6
- /** What a waiting message gives the turn in front to start running in Claw (`_queue_behind`). */
7
- export declare const SUBMIT_WAIT_SECONDS = 15;
8
6
  /** Teams on a phone or a tablet: its own Stop under a reply is greyed out, and its own thumbs never reach the worker. */
9
7
  export declare function isMobile(clientPlatform: string): boolean;
10
8
  /** A review the review service starts as a turn here (its item runs in Claw already; the turn only follows it). */
@@ -47,7 +45,8 @@ export interface StartTurn {
47
45
  /**
48
46
  * Starting and following a turn (the Python `Agent._start_turn`, `_queue_behind`, `_guarded`, `_record_session`,
49
47
  * `_record_turn`): a new task is admitted under the person's task limit; a message sent while the chat's turn runs is
50
- * queued in Claw behind it (one at most); a review has a session of its own and is neither queued nor queued behind.
48
+ * queued behind it (up to `QUEUED_TURNS_MAX`, in order, each told its place) and goes to Claw once every turn in front of it
49
+ * is over, so `/stop` can still give it up; a review has a session of its own and is neither queued nor queued behind.
51
50
  */
52
51
  export declare class TurnStarter {
53
52
  private readonly ctx;
@@ -57,11 +56,20 @@ export declare class TurnStarter {
57
56
  settled(): Promise<void>;
58
57
  /** The turn that runs (its end is `done`), or undefined when none was started (the person was told why). */
59
58
  start(request: StartTurn): Promise<RunningTurn | undefined>;
59
+ private track;
60
+ /** The turn goes to Claw now; its end is `done`. */
61
+ private launch;
62
+ /**
63
+ * A queued turn: it goes once every turn in front of it is over, or stopped (Claw took its interrupt: Claw is free for the next
64
+ * message, whether or not that turn ever reports its end); the caller does not wait for it. Not at all when it was given up
65
+ * meanwhile (`/stop`, said there).
66
+ */
67
+ private afterFront;
60
68
  /** True when admitted as the chat's first turn; false when the chat has one (queue behind it); undefined: refused. */
61
69
  private admit;
62
70
  /**
63
- * Registers `turn` behind the chat's running one; false (the person is told why) when it may not go. It is sent only
64
- * once Claw runs the turn in front: then Claw names that turn as the one it waits behind.
71
+ * Registers `turn` behind the chat's running one: "front" when the chat has none after all (it runs now), "waiting" when it
72
+ * is queued (the person is told its place), "refused" (and told why) when it may not wait.
65
73
  */
66
74
  private queueBehind;
67
75
  private sessionOf;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primusclaw/personal-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The personal agent's channel-neutral core: domain logic, ports and plugin interfaces.",
5
5
  "license": "MIT",
6
6
  "repository": {