@saadnvd1/agent-os 0.3.821 → 0.3.824

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.
@@ -1921,6 +1921,22 @@
1921
1921
  "area": "AgentOS",
1922
1922
  "text": "You can set your name in Settings, and AgentOS uses it in chats and notes instead of a fixed name.",
1923
1923
  "url": "https://github.com/saadnvd1/agent-os/pull/550"
1924
+ },
1925
+ {
1926
+ "id": 551,
1927
+ "date": "2026-11-01",
1928
+ "kind": "fixed",
1929
+ "area": "Chat",
1930
+ "text": "Questions from GitHub Copilot chats now always show up at the latest message after the chat restarts, instead of going missing.",
1931
+ "url": "https://github.com/saadnvd1/agent-os/pull/551"
1932
+ },
1933
+ {
1934
+ "id": 552,
1935
+ "date": "2026-11-01",
1936
+ "kind": "fixed",
1937
+ "area": "Chat",
1938
+ "text": "Cancelled questions from GitHub Copilot chats now clear right away instead of staying on screen.",
1939
+ "url": "https://github.com/saadnvd1/agent-os/pull/552"
1924
1940
  }
1925
1941
  ]
1926
1942
  }
@@ -66,6 +66,8 @@ export function askUserQuestion(a: AskArgs): ChatQuestion {
66
66
  };
67
67
  }
68
68
 
69
+ const requestKey = (id: string | number) => `${typeof id}:${id}`;
70
+
69
71
  const text = (t: string, isError = false): CallToolResult => ({
70
72
  content: [{ type: "text", text: t }],
71
73
  ...(isError ? { isError } : {}),
@@ -105,8 +107,10 @@ export function isAskUserCall(call: P): boolean {
105
107
  export class CopilotAskServer {
106
108
  private http?: http.Server;
107
109
  private secret = crypto.randomBytes(32).toString("hex");
108
- private next = 0;
109
110
  private live = new Set<{ close(): Promise<void> }>();
111
+ // Waiting ask_user calls by JSON-RPC request id. A cancel arrives as its
112
+ // own POST, on a fresh MCP server, so only this map can match it.
113
+ private waiting = new Map<string, string>();
110
114
 
111
115
  constructor(private pending: PendingApprovals) {}
112
116
 
@@ -119,22 +123,49 @@ export class CopilotAskServer {
119
123
  "Ask the user a question and wait for the answer. Use it when you need a decision or missing information instead of asking in plain text. Offer options for a choice (multiSelect to allow several); with no options the user types an answer. Blocks until they answer.",
120
124
  inputSchema: askInput,
121
125
  },
122
- async (args) => {
126
+ async (args, extra) => {
123
127
  const q = askUserQuestion(args);
124
- const o = await this.pending.ask({
125
- id: `approval-ask-${++this.next}`,
128
+ // Unique across worker restarts: a card's id is its chat row, so a
129
+ // reused id would rewrite an old card up the history instead.
130
+ const id = `approval-ask-${crypto.randomUUID()}`;
131
+ const asked = this.pending.ask({
132
+ id,
126
133
  toolName: "AskUserQuestion",
127
134
  title: "Questions",
128
135
  input: { questions: [q] },
129
136
  questions: [q],
130
137
  canAlways: false,
131
138
  });
132
- return askUserResult(q, o);
139
+ // Copilot gave up on the call (its timeout, or the connection went):
140
+ // nobody is waiting for this answer any more.
141
+ const expire = () => this.pending.respond(id, { decision: "expired" });
142
+ if (extra.signal.aborted) expire();
143
+ else extra.signal.addEventListener("abort", expire, { once: true });
144
+ const key = requestKey(extra.requestId);
145
+ this.waiting.set(key, id);
146
+ try {
147
+ return askUserResult(q, await asked);
148
+ } finally {
149
+ if (this.waiting.get(key) === id) this.waiting.delete(key);
150
+ }
133
151
  }
134
152
  );
135
153
  return server;
136
154
  }
137
155
 
156
+ // notifications/cancelled names the request to give up on. An id with no
157
+ // waiting call (answered already, or never ours) is ignored.
158
+ private cancelWaiting(body: unknown): void {
159
+ for (const m of Array.isArray(body) ? body : [body]) {
160
+ const msg = m as { method?: unknown; params?: { requestId?: unknown } };
161
+ const rid = msg?.params?.requestId;
162
+ if (msg?.method !== "notifications/cancelled") continue;
163
+ if (typeof rid !== "string" && typeof rid !== "number") continue;
164
+ const card = this.waiting.get(requestKey(rid));
165
+ if (card) this.pending.respond(card, { decision: "expired" });
166
+ }
167
+ }
168
+
138
169
  private allowed(req: http.IncomingMessage): number {
139
170
  if (req.headers.origin) return 403;
140
171
  const host = (req.headers.host ?? "").replace(/:\d+$/, "");
@@ -176,6 +207,7 @@ export class CopilotAskServer {
176
207
  res.writeHead(400).end();
177
208
  return;
178
209
  }
210
+ this.cancelWaiting(body);
179
211
  const server = this.build();
180
212
  const transport = new StreamableHTTPServerTransport({
181
213
  sessionIdGenerator: undefined,
@@ -222,6 +254,7 @@ export class CopilotAskServer {
222
254
  // and stops listening.
223
255
  close(): void {
224
256
  this.pending.expireAll();
257
+ this.waiting.clear();
225
258
  const server = this.http;
226
259
  this.http = undefined;
227
260
  server?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.821",
3
+ "version": "0.3.824",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"