@aion0/forge 0.10.26 → 0.10.27

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.
package/RELEASE_NOTES.md CHANGED
@@ -1,19 +1,14 @@
1
- # Forge v0.10.26
1
+ # Forge v0.10.27
2
2
 
3
3
  Released: 2026-06-02
4
4
 
5
- ## Changes since v0.10.25
5
+ ## Changes since v0.10.26
6
6
 
7
7
  ### Other
8
- - feat(http-auth): bearer-token-exchange 2-step API token cached JWT
9
- - fix(login-status): 2FA probe = passive SSH greet, not interactive 2fa_verify
10
- - fix(login-status): derive 2FA host from gitlab connector + skip uninstalled sources
11
- - fix(login-status): expand host_match settings tokens for open-login URL
12
- - fix(login-status): probe via lib, not self-fetch — fixes all-401 results
13
- - feat(login-status): central panel for all expirable auth credentials
14
- - feat(http): capture_response_headers + chrome-mcp.sh helper
15
- - fix(http): use undici.fetch for verify_tls path (v8 Agent incompatible with Node 22 bundled fetch)
16
- - feat(http): connector-level http.verify_tls knob for self-signed appliances
8
+ - fix(chat): atomic tool_use+tool_result cap + raise budget 8k32k
9
+ - fix(http): scope slash-collapse to pathname only (don't touch query/fragment)
10
+ - fix(http): collapse double-slash in URLs (base_url trailing-slash bug)
11
+ - fix(connector-test): wrap applyAuth + route handler so errors come back as JSON
17
12
 
18
13
 
19
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.25...v0.10.26
14
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.26...v0.10.27
@@ -11,7 +11,16 @@ import { runConnectorTest } from '@/lib/connectors/test-runner';
11
11
 
12
12
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
13
13
  const { id } = await params;
14
- const r = await runConnectorTest(id);
15
- const { code, ...body } = r;
16
- return NextResponse.json(body, code ? { status: code } : undefined);
14
+ try {
15
+ const r = await runConnectorTest(id);
16
+ const { code, ...body } = r;
17
+ return NextResponse.json(body, code ? { status: code } : undefined);
18
+ } catch (e) {
19
+ // Belt-and-suspenders: never let an uncaught exception bubble to
20
+ // Next's HTML 500 page — the browser would then choke on JSON parse.
21
+ return NextResponse.json(
22
+ { ok: false, error: `internal error: ${(e as Error).message}` },
23
+ { status: 500 },
24
+ );
25
+ }
17
26
  }
@@ -48,7 +48,10 @@ const MAX_TOKENS = 16000;
48
48
  // raw is summarized by the memory-standalone Temper Summary sub-task
49
49
  // and recalled via buildMemoryContext as compact blocks instead.
50
50
  const HISTORY_MSG_BUDGET = 60;
51
- const HISTORY_TOKEN_BUDGET = 8000;
51
+ // Bumped 8000 → 32000 — modern models all ≥ 200k context; 8000 was
52
+ // stripping single oversized tool results (e.g. mantis.search_bugs
53
+ // returning 20k chars), leaving history empty after orphan-trim.
54
+ const HISTORY_TOKEN_BUDGET = 32000;
52
55
  // Hard cap on a single tool_result stored into the conversation (chars).
53
56
  // A giant result (e.g. a connector returning a full test tree) would
54
57
  // otherwise blow the whole HISTORY_TOKEN_BUDGET, push its paired
@@ -122,13 +122,33 @@ function expandUrlPath(
122
122
  return out;
123
123
  }
124
124
 
125
+ /**
126
+ * Collapse consecutive slashes in the URL's pathname only — fixes the
127
+ * `https://host//api/...` artifact when settings.base_url has a trailing
128
+ * slash AND the manifest path starts with `/`. Query string and fragment
129
+ * are NOT touched (a query value like `?url=//foo` is left alone).
130
+ *
131
+ * If `url` doesn't parse, returns it unchanged.
132
+ */
133
+ function collapsePathSlashes(url: string): string {
134
+ try {
135
+ const u = new URL(url);
136
+ u.pathname = u.pathname.replace(/\/{2,}/g, '/');
137
+ return u.toString();
138
+ } catch {
139
+ return url;
140
+ }
141
+ }
142
+
125
143
  function buildUrl(
126
144
  spec: HttpRequestSpec,
127
145
  settings: Record<string, any>,
128
146
  args: Record<string, any>,
129
147
  paramSchemas?: Record<string, ConnectorFieldSchema>,
130
148
  ): string {
131
- const base = expandUrlPath(spec.url, settings, args, paramSchemas);
149
+ const base = collapsePathSlashes(
150
+ expandUrlPath(spec.url, settings, args, paramSchemas),
151
+ );
132
152
  if (!spec.query) return base;
133
153
  const url = new URL(base);
134
154
  for (const [k, raw] of Object.entries(spec.query)) {
@@ -175,7 +195,9 @@ async function exchangeBearerToken(
175
195
  const exp = (s: string | undefined) =>
176
196
  s == null ? '' : expandAllTokens(String(s), settings, args);
177
197
  const apiToken = exp(auth.api_token);
178
- const exchangeUrl = exp(auth.exchange_url);
198
+ // Same pathname-only collapse as buildUrl — guards against trailing
199
+ // slash on settings.base_url producing `host//api/...`.
200
+ const exchangeUrl = collapsePathSlashes(exp(auth.exchange_url));
179
201
  if (!apiToken) throw new Error('bearer-token-exchange: api_token is empty');
180
202
  if (!exchangeUrl) throw new Error('bearer-token-exchange: exchange_url is empty');
181
203
  const key = `${apiToken}|${exchangeUrl}`;
@@ -299,19 +299,42 @@ export function listMessagesCapped(
299
299
  SELECT * FROM chat_messages WHERE session_id = ?
300
300
  ORDER BY ts DESC LIMIT ?
301
301
  `).all(session_id, cap) as MessageRow[];
302
- const newestFirst = rows.map(rowToMessage);
302
+ const chrono = rows.map(rowToMessage).reverse();
303
303
 
304
- // Now apply tokenBudget walking newest oldest. Always keep at
305
- // least one (so an oversized last message doesn't strand the loop).
306
- const kept: Message[] = [];
304
+ // Group messages so that an assistant `tool_use` message and its
305
+ // paired user `tool_result` reply are an indivisible unit. Without
306
+ // this, the token-budget walk can keep the oversized tool_result
307
+ // but drop the tool_use that produced it — leading to the
308
+ // tool_result being treated as an orphan and stripped, leaving an
309
+ // empty history. Each group is chronological.
310
+ const groups: Message[][] = [];
311
+ for (let i = 0; i < chrono.length; i++) {
312
+ const m = chrono[i];
313
+ const hasToolUse = m.role === 'assistant' && m.blocks.some((b) => b.type === 'tool_use');
314
+ const next = chrono[i + 1];
315
+ const nextHasToolResult =
316
+ next && next.role === 'user' && next.blocks.some((b) => b.type === 'tool_result');
317
+ if (hasToolUse && nextHasToolResult) {
318
+ groups.push([m, next]);
319
+ i++; // skip the partner — we just consumed it
320
+ } else {
321
+ groups.push([m]);
322
+ }
323
+ }
324
+
325
+ // Walk groups newest-first, applying the token budget. Always keep
326
+ // at least one group so an oversized last group doesn't strand the
327
+ // loop (provider will see a single message — still valid).
328
+ const keptGroups: Message[][] = [];
307
329
  let used = 0;
308
- for (const m of newestFirst) {
309
- const cost = estimateTokens(m);
310
- if (kept.length > 0 && used + cost > tokenBudget) break;
311
- kept.push(m);
330
+ for (let i = groups.length - 1; i >= 0; i--) {
331
+ const g = groups[i];
332
+ const cost = g.reduce((s, m) => s + estimateTokens(m), 0);
333
+ if (keptGroups.length > 0 && used + cost > tokenBudget) break;
334
+ keptGroups.unshift(g);
312
335
  used += cost;
313
336
  }
314
- return kept.reverse();
337
+ return keptGroups.flat();
315
338
  }
316
339
 
317
340
  export function deleteMessage(id: string): boolean {
@@ -116,7 +116,11 @@ async function runHttpProbe(
116
116
  if (body != null && contentType && !headers.has('content-type')) {
117
117
  headers.set('content-type', contentType);
118
118
  }
119
- url = await applyAuth(url, headers, def.auth, effectiveSettings);
119
+ try {
120
+ url = await applyAuth(url, headers, def.auth, effectiveSettings);
121
+ } catch (e) {
122
+ return { ok: false, error: `auth setup failed: ${(e as Error).message}` };
123
+ }
120
124
 
121
125
  const timeoutMs = test.timeout_ms || DEFAULT_TIMEOUT_MS;
122
126
  const okStatus = test.ok_status?.length ? test.ok_status : [200];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {