@loicngr/kobo 1.11.1 → 1.11.3
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/CHANGELOG.md +36 -0
- package/README.md +6 -6
- package/dist/server/routes/workspaces.js +69 -1
- package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +1 -1
- package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -0
- package/dist/server/services/agent/engines/codex/engine.js +1 -1
- package/dist/server/services/agent/engines/codex/event-mapper.js +1 -1
- package/dist/server/services/agent/orchestrator.js +91 -3
- package/dist/server/services/forge/bitbucket/provider.js +291 -0
- package/dist/server/services/forge/github/provider.js +10 -1
- package/dist/server/services/forge/gitlab/provider.js +10 -1
- package/dist/server/services/forge/none.js +10 -1
- package/dist/server/services/forge/registry.js +3 -1
- package/dist/server/services/forge/resolve.js +4 -1
- package/dist/server/services/settings-defaults.js +1 -1
- package/dist/server/services/settings-service.js +68 -12
- package/dist/shared/project-colors.js +7 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DB_EucGW.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-CppcIG5K.js → ChangelogPage-CNn9l8Ht.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-B0-qi7Jo.js → ClosePopup-BycCSMYN.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-BnEu_-oB.js +2 -0
- package/src/client/dist/pwa/assets/{DiffViewer-COD5Mdp_.js → DiffViewer-DiV6QEdI.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-Bnad2BVm.js → HealthPage-ByBfOzgZ.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-Bwulksar.css → MainLayout-BY-xOjDK.css} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-nn4YIcy8.js → MainLayout-DOuqjNgy.js} +17 -17
- package/src/client/dist/pwa/assets/{QBtnToggle-CZXgD3Hf.js → QBtnToggle-BClaEu9N.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-BJH-iJCV.js → QChip-4nbXNOAl.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-BCz_9zhf.js +36 -0
- package/src/client/dist/pwa/assets/QTooltip-DSxTMMkV.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-BwD6K8qD.js → SearchPage-BRv1M8JZ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BqsWfOOK.js +16 -0
- package/src/client/dist/pwa/assets/{SettingsPage-DJEH2ahl.css → SettingsPage-Os-6b-6H.css} +1 -1
- package/src/client/dist/pwa/assets/{ThinkingItem-DBWuhjgW.js → ThinkingItem-Bx_u0wMq.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-txugr_lm.js → TouchPan-a3VbuxQ6.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-Cv04uh_0.css → WorkspacePage-D-1HoeQg.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Dsj0f4XV.js +4 -0
- package/src/client/dist/pwa/assets/{build-path-tree-BUkEDxlT.js → build-path-tree-BKKu-ryI.js} +1 -1
- package/src/client/dist/pwa/assets/{cssMode-BDLIXvw9.js → cssMode-BKAdI7ff.js} +1 -1
- package/src/client/dist/pwa/assets/documents-Cc-OXiwm.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api2-PhaZVUXg.js → editor.api2-BaCyAVGk.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.main-2nNvSQc1.js → editor.main-BbVQG_bB.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-HgUyAUBK.js → expand-template-CZOjiOqB.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-Chk4XGyr.js → formatters-CCEkVVIc.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-Dy02xxKp.js → freemarker2-Bh-TbpI2.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-Do59Qec5.js → handlebars-B8nDlL6P.js} +1 -1
- package/src/client/dist/pwa/assets/{html-MJ9-LLjS.js → html-7xHGi5EV.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-AsZ-SMzZ.js → htmlMode-Bj3pQot4.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-DXWy90AZ.js +1 -0
- package/src/client/dist/pwa/assets/index-CH7ykfX_.js +84 -0
- package/src/client/dist/pwa/assets/{javascript-DoZFpstd.js → javascript-BtBc5IVR.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-CMeFz2Y2.js → jsonMode-0IOmldeM.js} +1 -1
- package/src/client/dist/pwa/assets/{kobo-commands-DmJq1I6k.js → kobo-commands-2L1djzbe.js} +1 -1
- package/src/client/dist/pwa/assets/{layout-BSJkgTMQ.js → layout-DgfOrzwf.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Bc70ZdlV.js → liquid-DF94cTT4.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-Co4WnPYT.js → lspLanguageFeatures-iHqiZBzm.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-BsXN2I7z.js → mdx-C-x1eJF5.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-BpmQVvpQ.js → monaco.contribution-DSfodOAp.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-B0RCBQ5K.js → network-auth-DA6dNTa5.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-BiozufWe.js → pending-diff-open-bDBsLjPx.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-kRWgzgvi.js → permissionModes-DQd7fbSy.js} +1 -1
- package/src/client/dist/pwa/assets/project-color-CQtsHSqi.js +1 -0
- package/src/client/dist/pwa/assets/{python-CV5Zt8gJ.js → python-fAiLpu-5.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-4Ev2x3ig.js → razor-Clyexxhi.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-Cjt11WnX.js → render-chat-markdown-MzPqUxrD.js} +1 -1
- package/src/client/dist/pwa/assets/{tsMode-BDysTsn1.js → tsMode-aOX259X0.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DM1CefPN.js → typescript-BiPmygzZ.js} +1 -1
- package/src/client/dist/pwa/assets/{use-onboarding-D2BGKiP_.js → use-onboarding-NfnxjTzg.js} +1 -1
- package/src/client/dist/pwa/assets/{use-quasar-Cps11O9Y.js → use-quasar-CyeldpIw.js} +1 -1
- package/src/client/dist/pwa/assets/{workers-BuJZJ6j6.js → workers-BcOflh4r.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-DzARVQ9o.js → xml-6qooibMC.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-P0ZYSZvj.js → yaml-DzDdyl50.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-ByJjMo0k.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-D0xvdJAQ.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +0 -1
- package/src/client/dist/pwa/assets/QSelect-tsFTetLi.js +0 -36
- package/src/client/dist/pwa/assets/QTooltip-CA_Iyybc.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CF-xI5PO.js +0 -16
- package/src/client/dist/pwa/assets/WorkspacePage-DgucXHjd.js +0 -4
- package/src/client/dist/pwa/assets/documents-ClS4k_bT.js +0 -1
- package/src/client/dist/pwa/assets/i18n-Djv0PzK4.js +0 -1
- package/src/client/dist/pwa/assets/index-BpTtR9lB.js +0 -84
- package/src/client/dist/pwa/assets/project-color-D35ZMh9_.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.11.3
|
|
8
|
+
|
|
9
|
+
- feat(forge): add bitbucket community support
|
|
10
|
+
|
|
11
|
+
## 1.11.2
|
|
12
|
+
|
|
13
|
+
- feat(integrations): let Notion and Sentry be enabled independently, keep
|
|
14
|
+
their settings isolated, hide inactive project overrides, and add detailed
|
|
15
|
+
**Test connection** feedback.
|
|
16
|
+
- feat(chat): stream Claude output token-by-token like Codex, consolidate agent
|
|
17
|
+
reasoning into one live panel above the composer, and add a visibility
|
|
18
|
+
preference for reasoning and verbose system messages.
|
|
19
|
+
- feat(chat): add `Ctrl+J` for a new line, `Ctrl+Enter` to force a queued
|
|
20
|
+
message while an agent is busy, and keep the shortcuts visible below chat.
|
|
21
|
+
- feat(chat): add session-aware `Ctrl+F` workspace-history search, scrolling to
|
|
22
|
+
the selected result; global search now opens the same exact message.
|
|
23
|
+
- feat(chat): add a `Ctrl+K` workspace command palette for common actions.
|
|
24
|
+
- feat(chat): make worktree-relative paths and Edit/Write cards open the
|
|
25
|
+
corresponding file in the Git diff viewer.
|
|
26
|
+
- feat(git): add workspace commits with either a user-written message or an
|
|
27
|
+
agent request; add confirmed PR/MR merging and an optional remote-branch
|
|
28
|
+
deletion after merge.
|
|
29
|
+
- feat(auto-loop): retry quota, rate-limit, and temporary upstream failures;
|
|
30
|
+
add a configurable maximum retry count.
|
|
31
|
+
- feat(notifications): add `basic-notification.mp3`, event-level sound
|
|
32
|
+
inheritance, an agent-error sound, browser-permission diagnostics, and more
|
|
33
|
+
reliable playback while Kobo is in a background tab.
|
|
34
|
+
- feat(settings): extend the project color palette and improve save-state
|
|
35
|
+
handling for activity-feed preferences.
|
|
36
|
+
- feat(observability): enrich the session timeline with tools, tokens, and
|
|
37
|
+
errors; keep it fresh during an active run and retain diagnostic export.
|
|
38
|
+
- feat(pwa): show install, offline, update, and WebSocket-reconnection states;
|
|
39
|
+
preserve the visible chat area when the status banner is displayed.
|
|
40
|
+
- fix(websocket): avoid reconnecting after an explicit disconnect and resume
|
|
41
|
+
safely after network connectivity returns.
|
|
42
|
+
|
|
7
43
|
## 1.11.1
|
|
8
44
|
|
|
9
45
|
- feat(workspace): improve workspace actions
|
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
17
17
|
|
|
18
18
|
- **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
|
|
19
19
|
- **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
|
|
20
|
-
- **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
|
|
20
|
+
- **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. `Ctrl+K` opens a command palette for common workspace actions. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
|
|
21
21
|
- **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
|
|
22
22
|
|
|
23
23
|

|
|
24
|
-
- **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
|
|
24
|
+
- **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation, then Kōbō can offer to delete the merged remote branch. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), Bitbucket Community (`bkt`), or no forge, auto-detected from the remote and overridable per project.
|
|
25
25
|
|
|
26
26
|

|
|
27
27
|
- **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI. When a project has no configured server, the panel and MCP tool report that explicitly instead of implying one is running.
|
|
@@ -29,15 +29,15 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
29
29
|
- **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer. Choosing **Other** opens an inline free-form field, and option previews are available on hover. Codex native interactive questions require its `plan` permission mode.
|
|
30
30
|
|
|
31
31
|

|
|
32
|
-
- **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
|
|
32
|
+
- **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Rate-limit and temporary upstream failures retry on the quota backoff ladder; Settings controls the maximum number of automatic retries. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
|
|
33
33
|
- **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets sit in the footer, and sessions auto-resume after a rate-limit reset.
|
|
34
34
|
- **Scheduled wakeups**: the agent schedules a one-shot wake-up via the `schedule_wakeup` MCP tool. Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
|
|
35
35
|
- **Cron schedules**: recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`). Each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
|
|
36
36
|
- **Lifecycle scripts**: shell scripts run automatically at key moments — **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
|
|
37
37
|
- **Disk-space purge**: free a merged workspace's disk space without losing its chat history — see [below](#disk-space-purge).
|
|
38
|
-
- **Observability**: a compact session timeline in the right panel shows session duration and
|
|
38
|
+
- **Observability**: a compact session timeline in the right panel shows each session’s duration, tools, tokens, and errors; download a redacted workspace diagnostic JSON for troubleshooting.
|
|
39
39
|
- **Optional integrations**: independently enable or disable Notion (import missions) and Sentry (fix from issue URL) in Settings, with a **Test connection** action for each; local voice transcription uses whisper.cpp.
|
|
40
|
-
- **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
|
|
40
|
+
- **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state, WebSocket reconnection, and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
|
|
41
41
|
|
|
42
42
|
## Quick start
|
|
43
43
|
|
|
@@ -84,7 +84,7 @@ The most common knobs:
|
|
|
84
84
|
| `ANTHROPIC_API_KEY` | none | Claude Code engine credential (alternative to `claude /login`) |
|
|
85
85
|
| `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
|
|
86
86
|
|
|
87
|
-
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
|
|
87
|
+
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. **Settings → Forge** holds local Bitbucket Community credentials for `bkt`; Kōbō uses them for PR actions and provides them only to agents working in Bitbucket projects. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
|
|
88
88
|
|
|
89
89
|
The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
|
|
90
90
|
|
|
@@ -1559,6 +1559,48 @@ function readableHistorySearchResult(type, rawPayload, query) {
|
|
|
1559
1559
|
snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
|
|
1560
1560
|
};
|
|
1561
1561
|
}
|
|
1562
|
+
// GET /api/workspaces/:id/session-metrics — compact observability data for the timeline.
|
|
1563
|
+
app.get('/:id/session-metrics', (c) => {
|
|
1564
|
+
try {
|
|
1565
|
+
const id = c.req.param('id');
|
|
1566
|
+
if (!workspaceService.getWorkspace(id))
|
|
1567
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1568
|
+
const db = getDb();
|
|
1569
|
+
const sessions = workspaceService.listSessions(id);
|
|
1570
|
+
const metrics = new Map(sessions.map((session) => [
|
|
1571
|
+
session.id,
|
|
1572
|
+
{ sessionId: session.id, toolCalls: 0, errors: 0, inputTokens: 0, outputTokens: 0 },
|
|
1573
|
+
]));
|
|
1574
|
+
const events = db
|
|
1575
|
+
.prepare("SELECT session_id, payload FROM ws_events WHERE workspace_id = ? AND type = 'agent:event' AND session_id IS NOT NULL")
|
|
1576
|
+
.all(id);
|
|
1577
|
+
for (const row of events) {
|
|
1578
|
+
const metric = metrics.get(row.session_id);
|
|
1579
|
+
if (!metric)
|
|
1580
|
+
continue;
|
|
1581
|
+
try {
|
|
1582
|
+
const event = JSON.parse(row.payload);
|
|
1583
|
+
if (event.kind === 'tool:call')
|
|
1584
|
+
metric.toolCalls++;
|
|
1585
|
+
if (event.kind === 'error' || (event.kind === 'tool:result' && event.isError === true))
|
|
1586
|
+
metric.errors++;
|
|
1587
|
+
if (event.kind === 'usage') {
|
|
1588
|
+
if (typeof event.inputTokens === 'number')
|
|
1589
|
+
metric.inputTokens = Math.max(metric.inputTokens, event.inputTokens);
|
|
1590
|
+
if (typeof event.outputTokens === 'number')
|
|
1591
|
+
metric.outputTokens = Math.max(metric.outputTokens, event.outputTokens);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
// Corrupt historical events are intentionally ignored in diagnostics.
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return c.json({ sessions, metrics: [...metrics.values()] });
|
|
1599
|
+
}
|
|
1600
|
+
catch (err) {
|
|
1601
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1562
1604
|
// GET /api/workspaces/:id/diagnostic.json — portable, redacted-by-design event diagnostic.
|
|
1563
1605
|
app.get('/:id/diagnostic.json', (c) => {
|
|
1564
1606
|
try {
|
|
@@ -3186,7 +3228,8 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
3186
3228
|
return c.json({ error: 'This PR is not ready to merge', code: 'pr_not_ready' }, 409);
|
|
3187
3229
|
}
|
|
3188
3230
|
await provider.mergeRequest(workspace.worktreePath, snapshot.number);
|
|
3189
|
-
|
|
3231
|
+
const merged = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
|
|
3232
|
+
return c.json({ success: true, merged: merged?.state === 'MERGED', branch: workspace.workingBranch });
|
|
3190
3233
|
}
|
|
3191
3234
|
catch (err) {
|
|
3192
3235
|
if (err instanceof ForgeUnavailableError) {
|
|
@@ -3196,6 +3239,31 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
3196
3239
|
return c.json({ error: message }, 500);
|
|
3197
3240
|
}
|
|
3198
3241
|
});
|
|
3242
|
+
/** Delete the workspace branch from the remote after its PR/MR was merged. */
|
|
3243
|
+
app.post('/:id/delete-remote-branch', async (c) => {
|
|
3244
|
+
try {
|
|
3245
|
+
const id = c.req.param('id');
|
|
3246
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
3247
|
+
if (!workspace)
|
|
3248
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3249
|
+
const provider = getForgeProvider(resolveForge(workspace.projectPath));
|
|
3250
|
+
if (!provider.capabilities.canDeleteRemoteBranch) {
|
|
3251
|
+
return c.json({ error: 'This project cannot delete remote branches', code: 'forge_unsupported' }, 409);
|
|
3252
|
+
}
|
|
3253
|
+
const availability = await provider.isAvailable(workspace.worktreePath);
|
|
3254
|
+
if (!availability.available)
|
|
3255
|
+
return c.json({ error: 'Forge CLI unavailable', code: 'forge_unavailable' }, 409);
|
|
3256
|
+
const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
|
|
3257
|
+
if (snapshot?.state !== 'MERGED')
|
|
3258
|
+
return c.json({ error: 'The PR is not merged yet', code: 'pr_not_merged' }, 409);
|
|
3259
|
+
await provider.deleteRemoteBranch(workspace.worktreePath, workspace.workingBranch);
|
|
3260
|
+
return c.json({ success: true });
|
|
3261
|
+
}
|
|
3262
|
+
catch (err) {
|
|
3263
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3264
|
+
return c.json({ error: message }, 500);
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
3199
3267
|
/** Re-target a workspace onto a new source branch (metadata + worktree + PR base). */
|
|
3200
3268
|
app.post('/:id/change-source-branch', async (c) => {
|
|
3201
3269
|
const id = c.req.param('id');
|
|
@@ -199,6 +199,7 @@ export function createClaudeCodeEngine() {
|
|
|
199
199
|
console.warn(`[claude-engine stderr] ${data}`);
|
|
200
200
|
}
|
|
201
201
|
},
|
|
202
|
+
env: options.env,
|
|
202
203
|
});
|
|
203
204
|
sdkOptions.abortController = abortController;
|
|
204
205
|
// Override the SDK's libc-blind binary resolution on Linux glibc — see
|
|
@@ -76,7 +76,7 @@ function normalizeRateLimitInfo(info) {
|
|
|
76
76
|
* Anthropic's surfaces (`rate_limit_exceeded`, `Claude AI usage limit
|
|
77
77
|
* reached`, `You're out of extra usage`, `quota exceeded`).
|
|
78
78
|
*/
|
|
79
|
-
export const QUOTA_PATTERN =
|
|
79
|
+
export const QUOTA_PATTERN = /\b429\b|out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
|
|
80
80
|
export function createMapperState() {
|
|
81
81
|
return {
|
|
82
82
|
sessionStartedEmitted: false,
|
|
@@ -60,7 +60,7 @@ export function createCodexEngine() {
|
|
|
60
60
|
};
|
|
61
61
|
const streamingBatcher = createStreamingBatcher(emitDirect);
|
|
62
62
|
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
63
|
-
const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
|
|
63
|
+
const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
|
|
64
64
|
if (child.stderr) {
|
|
65
65
|
child.stderr.setEncoding('utf8');
|
|
66
66
|
child.stderr.on('data', (chunk) => {
|
|
@@ -7,7 +7,7 @@ import { nanoid } from 'nanoid';
|
|
|
7
7
|
// - "quota exceeded" / "insufficient_quota" / "insufficient quota"
|
|
8
8
|
// - "out of extra usage" / "out of usage"
|
|
9
9
|
// - "usage limit exceeded"
|
|
10
|
-
export const QUOTA_PATTERN =
|
|
10
|
+
export const QUOTA_PATTERN = /\b429\b|rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
|
|
11
11
|
export function createMapperState(opts) {
|
|
12
12
|
return {
|
|
13
13
|
sessionStartedEmitted: false,
|
|
@@ -6,8 +6,9 @@ import { unregisterProcess } from '../../utils/process-tracker.js';
|
|
|
6
6
|
import * as autoLoopService from '../auto-loop-service.js';
|
|
7
7
|
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
8
8
|
import * as cronService from '../cron-service.js';
|
|
9
|
+
import { resolveForge } from '../forge/resolve.js';
|
|
9
10
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
10
|
-
import { getEffectiveSettings } from '../settings-service.js';
|
|
11
|
+
import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
|
|
11
12
|
import { refreshNow } from '../usage/poller.js';
|
|
12
13
|
import * as wakeupService from '../wakeup-service.js';
|
|
13
14
|
import { emit, emitEphemeral } from '../websocket-service.js';
|
|
@@ -342,6 +343,38 @@ function buildMcpServers(workspaceId) {
|
|
|
342
343
|
},
|
|
343
344
|
];
|
|
344
345
|
}
|
|
346
|
+
function isBitbucketProject(projectPath) {
|
|
347
|
+
try {
|
|
348
|
+
return resolveForge(projectPath) === 'bitbucket-community';
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function buildAgentEnv(projectPath) {
|
|
355
|
+
if (!isBitbucketProject(projectPath))
|
|
356
|
+
return undefined;
|
|
357
|
+
const global = getGlobalSettings();
|
|
358
|
+
if (!global.bitbucketToken)
|
|
359
|
+
return undefined;
|
|
360
|
+
return {
|
|
361
|
+
...process.env,
|
|
362
|
+
BKT_HOST: 'https://bitbucket.org',
|
|
363
|
+
BKT_TOKEN: global.bitbucketToken,
|
|
364
|
+
BKT_USERNAME: global.bitbucketUsername,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function buildAgentPrompt(prompt, projectPath) {
|
|
368
|
+
if (!projectPath || !isBitbucketProject(projectPath))
|
|
369
|
+
return prompt;
|
|
370
|
+
return [
|
|
371
|
+
'[Kōbō Bitbucket] Bitbucket credentials are available through the `bkt` CLI in this session.',
|
|
372
|
+
'Use `bkt pr edit <number> --body <description>` and `bkt pr comment <number> --body <comment>` for PR updates.',
|
|
373
|
+
'Do not call the Bitbucket REST API with curl or reuse BKT_TOKEN as a Bearer token: Bitbucket Cloud API tokens require the bkt CLI authentication flow.',
|
|
374
|
+
'',
|
|
375
|
+
prompt,
|
|
376
|
+
].join('\n');
|
|
377
|
+
}
|
|
345
378
|
function resolveSessionForResume(workspaceId, existingSessionId, model) {
|
|
346
379
|
const db = getDb();
|
|
347
380
|
let lastSession;
|
|
@@ -540,6 +573,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
540
573
|
if (ev.kind === 'error' && ev.category === 'quota') {
|
|
541
574
|
void handleQuota(workspaceId, agentSessionId);
|
|
542
575
|
}
|
|
576
|
+
if (ev.kind === 'error' &&
|
|
577
|
+
ev.category === 'other' &&
|
|
578
|
+
TRANSIENT_SERVER_ERROR_PATTERN.test(ev.message) &&
|
|
579
|
+
getWs(workspaceId)?.autoLoop) {
|
|
580
|
+
void handleTransientAutoLoopFailure(workspaceId);
|
|
581
|
+
}
|
|
543
582
|
if (ev.kind === 'error' && ev.category === 'resume_failed') {
|
|
544
583
|
resumeFailedSet.add(workspaceId);
|
|
545
584
|
clearStaleEngineSessionId(workspaceId);
|
|
@@ -755,7 +794,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
755
794
|
const options = {
|
|
756
795
|
workspaceId,
|
|
757
796
|
workingDir,
|
|
758
|
-
prompt,
|
|
797
|
+
prompt: buildAgentPrompt(prompt, ws?.projectPath),
|
|
759
798
|
model,
|
|
760
799
|
effort: reasoningEffort,
|
|
761
800
|
// Cascade: explicit caller override → workspace setting → 'bypass'.
|
|
@@ -772,6 +811,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
772
811
|
})(),
|
|
773
812
|
settings,
|
|
774
813
|
mcpServers: buildMcpServers(workspaceId),
|
|
814
|
+
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
775
815
|
};
|
|
776
816
|
const controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, ev));
|
|
777
817
|
controllers.set(workspaceId, controller);
|
|
@@ -1195,6 +1235,8 @@ function clearStaleEngineSessionId(workspaceId) {
|
|
|
1195
1235
|
console.error('[orchestrator] Failed to clear stale engine session ID:', err);
|
|
1196
1236
|
}
|
|
1197
1237
|
}
|
|
1238
|
+
/** Retryable upstream failures for an auto-loop iteration (not coding errors). */
|
|
1239
|
+
export const TRANSIENT_SERVER_ERROR_PATTERN = /\b(?:http\s*)?500\b|internal server error|service unavailable|temporarily unavailable|overloaded/i;
|
|
1198
1240
|
const QUOTA_SAFETY_MARGIN_MS = 30_000;
|
|
1199
1241
|
const QUOTA_MAX_BACKOFF_MS = 24 * 60 * 60 * 1000;
|
|
1200
1242
|
const QUOTA_SATURATION_THRESHOLD_PCT = 95;
|
|
@@ -1214,7 +1256,11 @@ const QUOTA_FALLBACK_LADDER_MINUTES = [15, 30, 60, 180, 300];
|
|
|
1214
1256
|
* finally (2) a fixed ladder (15 → 30 → 60 → 180 → 300 min) whenever neither
|
|
1215
1257
|
* source is usable.
|
|
1216
1258
|
*/
|
|
1217
|
-
export async function computeQuotaBackoffMs(workspaceId, retryCount) {
|
|
1259
|
+
export async function computeQuotaBackoffMs(workspaceId, retryCount, preferQuotaReset = true) {
|
|
1260
|
+
if (!preferQuotaReset) {
|
|
1261
|
+
const idx = Math.min(Math.max(0, retryCount), QUOTA_FALLBACK_LADDER_MINUTES.length - 1);
|
|
1262
|
+
return { delayMs: QUOTA_FALLBACK_LADDER_MINUTES[idx] * 60 * 1000, source: 'fallback_ladder' };
|
|
1263
|
+
}
|
|
1218
1264
|
// 1. Prefer the rate_limit event from the agent stream — most recent + most precise.
|
|
1219
1265
|
const info = latestRateLimitInfo.get(workspaceId);
|
|
1220
1266
|
if (info?.buckets?.length) {
|
|
@@ -1270,14 +1316,56 @@ async function handleQuota(workspaceId, _agentSessionId) {
|
|
|
1270
1316
|
// May fail if transition is not valid
|
|
1271
1317
|
}
|
|
1272
1318
|
const retryCount = retryCounts.get(workspaceId) ?? 0;
|
|
1319
|
+
const autoLoopEnabled = getWs(workspaceId)?.autoLoop === true;
|
|
1320
|
+
const maxRetries = autoLoopEnabled ? (getGlobalSettings().autoLoopMaxRetries ?? 5) : 5;
|
|
1321
|
+
if (autoLoopEnabled && retryCount >= maxRetries) {
|
|
1322
|
+
autoLoopService.disable(workspaceId, 'error');
|
|
1323
|
+
try {
|
|
1324
|
+
updateWorkspaceStatus(workspaceId, 'error');
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
// The loop is disabled even if an already-terminal status rejects this transition.
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1273
1331
|
const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount);
|
|
1274
1332
|
retryCounts.set(workspaceId, retryCount + 1);
|
|
1275
1333
|
// The quotaBackoffService owns the timer + the persistent row + the
|
|
1276
1334
|
// 'agent:quota-backoff' WS emit. Hand off everything to it.
|
|
1277
1335
|
quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
|
|
1278
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Use the same persisted retry path for temporary upstream failures (HTTP 500
|
|
1339
|
+
* and overloads) while deliberately ignoring quota-reset data: a server error
|
|
1340
|
+
* is unrelated to the user's quota window and follows the fallback ladder.
|
|
1341
|
+
*/
|
|
1342
|
+
async function handleTransientAutoLoopFailure(workspaceId) {
|
|
1343
|
+
try {
|
|
1344
|
+
updateWorkspaceStatus(workspaceId, 'quota');
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
// May fail if transition is not valid.
|
|
1348
|
+
}
|
|
1349
|
+
const retryCount = retryCounts.get(workspaceId) ?? 0;
|
|
1350
|
+
const maxRetries = getGlobalSettings().autoLoopMaxRetries ?? 5;
|
|
1351
|
+
if (retryCount >= maxRetries) {
|
|
1352
|
+
autoLoopService.disable(workspaceId, 'error');
|
|
1353
|
+
try {
|
|
1354
|
+
updateWorkspaceStatus(workspaceId, 'error');
|
|
1355
|
+
}
|
|
1356
|
+
catch {
|
|
1357
|
+
// The loop is disabled even if an already-terminal status rejects this transition.
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount, false);
|
|
1362
|
+
retryCounts.set(workspaceId, retryCount + 1);
|
|
1363
|
+
quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
|
|
1364
|
+
}
|
|
1279
1365
|
/** @internal test-only — re-export of `handleQuota` for direct testing. */
|
|
1280
1366
|
export const _handleQuota = handleQuota;
|
|
1367
|
+
/** @internal test-only — re-export of the transient auto-loop retry handler. */
|
|
1368
|
+
export const _handleTransientAutoLoopFailure = handleTransientAutoLoopFailure;
|
|
1281
1369
|
/**
|
|
1282
1370
|
* Rebuild the in-memory `retryCounts` map from the persisted `pending_quota_backoffs`
|
|
1283
1371
|
* rows. Called from `index.ts` at boot, before `quotaBackoffService.restoreOnBoot`.
|