@loicngr/kobo 1.11.42 → 1.11.44
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/AGENTS.md +49 -4
- package/CHANGELOG.md +10 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +31 -74
- package/dist/mcp-server/kobo-tasks-server.js +51 -4
- package/dist/server/db/auto-loop-schema.js +44 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/review-return-schema.js +10 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/index.js +10 -9
- package/dist/server/routes/auto-loop-messages.js +51 -0
- package/dist/server/routes/workspaces.js +46 -136
- package/dist/server/services/agent/engines/claude-code/engine.js +2 -0
- package/dist/server/services/agent/engines/codex/engine.js +91 -31
- package/dist/server/services/agent/orchestrator.js +242 -99
- package/dist/server/services/agent/session-controller.js +42 -2
- package/dist/server/services/auto-loop-message-service.js +79 -0
- package/dist/server/services/auto-loop-service.js +225 -86
- package/dist/server/services/auto-loop-state-service.js +30 -0
- package/dist/server/services/cron-service.js +30 -16
- package/dist/server/services/quota-backoff-service.js +42 -10
- package/dist/server/services/review-return-service.js +64 -0
- package/dist/server/services/review-service.js +187 -0
- package/dist/server/services/settings-service.js +3 -3
- package/dist/server/services/task-mutations.js +186 -0
- package/dist/server/services/wakeup-service.js +16 -1
- package/dist/server/services/workspace-message-service.js +16 -2
- package/dist/server/services/workspace-service.js +25 -36
- package/dist/shared/auto-loop-prompts.js +22 -11
- package/dist/shared/auto-loop-types.js +1 -0
- package/dist/shared/review.js +6 -0
- package/dist/shared/task-verification.js +11 -0
- package/package.json +12 -12
- package/src/client/dist/pwa/assets/{ActivityFeed-CyCGH0Zr.js → ActivityFeed-D_y4aikp.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-B6xjI-I4.js → ChangelogPage-DSKCvpr2.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-BmRzCu1c.js → ComparisonPanel-DDxcsxyC.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-DHe0x63K.js → CreatePage-DKVEknbL.js} +2 -2
- package/src/client/dist/pwa/assets/{DashboardPage-CRTkx9P-.js → DashboardPage-Dg2s9nnv.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-BJfLiYGs.js → DiffViewer-BSzNScIr.js} +4 -4
- package/src/client/dist/pwa/assets/{DocumentsPanel-B9jjzoqY.js → DocumentsPanel-DfEoI_f7.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-CBZpPa30.js → DrawerToggleButton-D85PwhKb.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-CuG8MlgF.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-9VEvoR_4.js → HealthPage-CaLCpejR.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-CDxYIJ_s.js +2 -0
- package/src/client/dist/pwa/assets/MainLayout-DTLNunOi.css +1 -0
- package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
- package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
- package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-Cnkq_5eu.js → QBtnDropdown-qJWGcZpC.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
- package/src/client/dist/pwa/assets/QDrawer-DX3PhMqC.js +1 -0
- package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
- package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
- package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
- package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-CY4TWW_j.js +1 -0
- package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-WIoDeLoZ.js → QTooltip-cDK5AOQ5.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-tVggDx-C.js → SchedulePanel-LvHiZ6ZH.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-l4m1qxKs.js → SearchPage-BO-J4aNc.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-msHMBS7q.js → SettingsPage-Aw89fRSq.js} +2 -2
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-AXMYd-9b.js → SplitWorkspacePage-BIEceNwH.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CENXl8po.js → TerminalPanel-DitOCad1.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-DaEpS0Vf.js → TouchPan-DvPAhZsa.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-U3mwT0fV.js → TourReplayButton-CJYdH3Jf.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CpTA6VGb.js → WorkspaceAttentionLabels-CY-9z9nw.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DAVo-E4p.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-xH_OyA3r.js +4 -0
- package/src/client/dist/pwa/assets/{browser-B4pgwnXH.js → browser-BsB6cFv4.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-CW-ER1Mt.js +1 -0
- package/src/client/dist/pwa/assets/de-Mu7biceO.js +17 -0
- package/src/client/dist/pwa/assets/documents-SCd3w8xV.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-qQNqM6Y6.js → editor.api-BUdM7I6y.js} +1 -1
- package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
- package/src/client/dist/pwa/assets/efforts-DTJQP9or.js +9 -0
- package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
- package/src/client/dist/pwa/assets/engineFeatures-Bj6Ob41d.js +1 -0
- package/src/client/dist/pwa/assets/es-BY5IfNI2.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-SM8vvYyO.js +1 -0
- package/src/client/dist/pwa/assets/{format-BVgznNN8.js → format-BjQxf4pe.js} +3 -3
- package/src/client/dist/pwa/assets/{formatters-DV1VAk5B.js → formatters-BejhmNTV.js} +1 -1
- package/src/client/dist/pwa/assets/fr-BMXCzFCM.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-Nt_Z9A9p.js → freemarker2-5d5GhyRp.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-sJjXCA98.js → handlebars-DaafEsDK.js} +1 -1
- package/src/client/dist/pwa/assets/{html-B2wF0NKO.js → html-CwHBDd2A.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CgWW5vfb.js +1 -0
- package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
- package/src/client/dist/pwa/assets/index-Di9Sz5E8.js +20 -0
- package/src/client/dist/pwa/assets/it-ByPZQK_K.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-BPbCoYVZ.js → javascript-DSNIEEH5.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-DBwLDEnw.js → liquid-Dma1UpA1.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D_Sphj-P.js → mdx-D8rqEAws.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-BBjimLYs.js → monaco-contributions-2E7O2zoi.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DNP7HNKq.js → monaco.contribution-DqOgo72G.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-BLGlHVxR.js → network-auth-DNmhJTZn.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-tvaTlaFw.js → notify-theme-B6Hiwvwb.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-CxtcrwVN.js → pending-diff-open-CeiVIFZn.js} +1 -1
- package/src/client/dist/pwa/assets/permissionModes-DWdtf81n.js +36 -0
- package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
- package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-D75XmBNA.js → project-color-BCp9SWoI.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BdBTal4f.js → python-f-9NqRlI.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-gJNlRRte.js → razor-DsCmoX0h.js} +1 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-DK5BD9mU.js +67 -0
- package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DAGixRVW.js → typescript-cEHg4L10.js} +1 -1
- package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
- package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
- package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
- package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-BmTbosmB.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-BoGUaV1C.js → workspace-templates-DUWDLMIX.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BC9pt2ZZ.js → xml-Plt-Y7we.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CQpOdW-l.js → yaml-ScEmaAeN.js} +1 -1
- package/src/client/dist/pwa/index.html +10 -10
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +68 -104
- package/src/mcp-server/kobo-tasks-server.ts +56 -4
- package/src/client/dist/pwa/assets/GitPanel-gqySqt7G.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BM14JTBB.js +0 -2
- package/src/client/dist/pwa/assets/MainLayout-Dhj28mOe.css +0 -1
- package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
- package/src/client/dist/pwa/assets/QDrawer-C3b-lbmv.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-DJL6yy1E.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DE6-wlf7.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-6ENXqh7O.js +0 -1
- package/src/client/dist/pwa/assets/de-D9gPXN_a.js +0 -17
- package/src/client/dist/pwa/assets/documents-D88lOIxR.js +0 -1
- package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
- package/src/client/dist/pwa/assets/efforts-CMQi0Rp2.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-BPcXoXFd.js +0 -1
- package/src/client/dist/pwa/assets/es-B4Q1ncyP.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-DuMdsD-k.js +0 -1
- package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/src/client/dist/pwa/assets/fr-Qv-edVJn.js +0 -17
- package/src/client/dist/pwa/assets/i18n-eiyc92m_.js +0 -1
- package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
- package/src/client/dist/pwa/assets/index-DtsrXdzx.js +0 -20
- package/src/client/dist/pwa/assets/it-D-qzfo9T.js +0 -17
- package/src/client/dist/pwa/assets/permissionModes-CESk18Vk.js +0 -36
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-K3tUW_1p.js +0 -67
- package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CS5oBFk6.js +0 -2
|
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
4
4
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
5
5
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
6
|
+
import autoLoopMessagesRoutes from './auto-loop-messages.js';
|
|
6
7
|
const execFileAsync = promisify(execFileCb);
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -34,11 +35,12 @@ import * as notionService from '../services/notion-service.js';
|
|
|
34
35
|
import { renderPrTemplate } from '../services/pr-template-service.js';
|
|
35
36
|
import { clearPrSnapshotCache, getAllGitStats, getAllPrSnapshots, refreshPrSnapshot, } from '../services/pr-watcher-service.js';
|
|
36
37
|
import * as quotaBackoffService from '../services/quota-backoff-service.js';
|
|
37
|
-
import {
|
|
38
|
+
import { ReviewRequestError, startWorkspaceReview } from '../services/review-service.js';
|
|
38
39
|
import * as sentryService from '../services/sentry-service.js';
|
|
39
40
|
import * as settingsService from '../services/settings-service.js';
|
|
40
41
|
import { runSetupScript } from '../services/setup-script-service.js';
|
|
41
42
|
import { getSuitePrompts } from '../services/skill-suite-prompts.js';
|
|
43
|
+
import { TaskValidationError } from '../services/task-mutations.js';
|
|
42
44
|
import * as terminalService from '../services/terminal-service.js';
|
|
43
45
|
import * as wakeupService from '../services/wakeup-service.js';
|
|
44
46
|
import * as wsService from '../services/websocket-service.js';
|
|
@@ -1601,9 +1603,7 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1601
1603
|
const out = {};
|
|
1602
1604
|
for (const r of rows) {
|
|
1603
1605
|
out[r.id] = {
|
|
1604
|
-
|
|
1605
|
-
auto_loop_ready: r.auto_loop_ready === 1,
|
|
1606
|
-
no_progress_streak: r.no_progress_streak,
|
|
1606
|
+
...autoLoopService.getStatus(r.id),
|
|
1607
1607
|
tasks_done: r.tasks_done ?? 0,
|
|
1608
1608
|
tasks_total: r.tasks_total ?? 0,
|
|
1609
1609
|
crons_count: r.crons_count ?? 0,
|
|
@@ -1616,6 +1616,7 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1616
1616
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
1617
1617
|
}
|
|
1618
1618
|
});
|
|
1619
|
+
app.route('/', autoLoopMessagesRoutes);
|
|
1619
1620
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1620
1621
|
app.get('/:id/auto-loop', (c) => {
|
|
1621
1622
|
try {
|
|
@@ -2011,67 +2012,68 @@ app.delete('/:id/events/:eventId', (c) => {
|
|
|
2011
2012
|
app.post('/:id/tasks', async (c) => {
|
|
2012
2013
|
try {
|
|
2013
2014
|
const id = c.req.param('id');
|
|
2014
|
-
|
|
2015
|
-
if (!workspace) {
|
|
2015
|
+
if (!workspaceService.getWorkspace(id))
|
|
2016
2016
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
.json()
|
|
2020
|
-
.catch(() => ({}));
|
|
2021
|
-
if (!body.title?.trim()) {
|
|
2017
|
+
const body = await c.req.json().catch(() => null);
|
|
2018
|
+
if (!body || typeof body.title !== 'string' || !body.title.trim()) {
|
|
2022
2019
|
return c.json({ error: 'Title is required' }, 400);
|
|
2023
2020
|
}
|
|
2024
|
-
const existing = workspaceService.listTasks(id);
|
|
2025
|
-
const nextSortOrder = existing.length > 0 ? Math.max(...existing.map((t) => t.sortOrder)) + 1 : 0;
|
|
2026
2021
|
const task = workspaceService.createTask(id, {
|
|
2027
2022
|
title: body.title.trim(),
|
|
2028
|
-
isAcceptanceCriterion:
|
|
2029
|
-
sortOrder:
|
|
2023
|
+
isAcceptanceCriterion: body.isAcceptanceCriterion,
|
|
2024
|
+
sortOrder: body.sortOrder,
|
|
2025
|
+
afterTaskId: body.afterTaskId,
|
|
2026
|
+
role: body.role,
|
|
2030
2027
|
});
|
|
2031
2028
|
return c.json(task, 201);
|
|
2032
2029
|
}
|
|
2033
2030
|
catch (err) {
|
|
2034
2031
|
const message = err instanceof Error ? err.message : String(err);
|
|
2035
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2032
|
+
return c.json({ error: message }, err instanceof TaskValidationError ? 400 : workspaceErrorStatus(err));
|
|
2036
2033
|
}
|
|
2037
2034
|
});
|
|
2038
|
-
// PATCH /api/workspaces/:id/tasks/:taskId —
|
|
2035
|
+
// PATCH /api/workspaces/:id/tasks/:taskId — validate and apply every field atomically
|
|
2039
2036
|
app.patch('/:id/tasks/:taskId', async (c) => {
|
|
2040
2037
|
try {
|
|
2041
2038
|
const id = c.req.param('id');
|
|
2042
2039
|
const taskId = c.req.param('taskId');
|
|
2043
|
-
|
|
2044
|
-
if (!workspace) {
|
|
2040
|
+
if (!workspaceService.getWorkspace(id))
|
|
2045
2041
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2046
|
-
|
|
2047
|
-
const task = workspaceService.getTask(taskId, id);
|
|
2048
|
-
if (!task) {
|
|
2042
|
+
if (!workspaceService.getTask(taskId, id)) {
|
|
2049
2043
|
return c.json({ error: `Task '${taskId}' not found in workspace '${id}'` }, 404);
|
|
2050
2044
|
}
|
|
2051
|
-
const body = await c.req
|
|
2052
|
-
|
|
2053
|
-
.
|
|
2054
|
-
if (body.status === undefined && body.title === undefined) {
|
|
2055
|
-
return c.json({ error: 'At least one of status or title is required' }, 400);
|
|
2056
|
-
}
|
|
2057
|
-
if (body.title !== undefined) {
|
|
2058
|
-
if (!body.title.trim()) {
|
|
2059
|
-
return c.json({ error: 'Title cannot be empty' }, 400);
|
|
2060
|
-
}
|
|
2061
|
-
workspaceService.updateTaskTitle(taskId, body.title.trim());
|
|
2062
|
-
}
|
|
2063
|
-
if (body.status !== undefined) {
|
|
2064
|
-
const validStatuses = ['pending', 'in_progress', 'done'];
|
|
2065
|
-
if (!validStatuses.includes(body.status)) {
|
|
2066
|
-
return c.json({ error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }, 400);
|
|
2067
|
-
}
|
|
2068
|
-
workspaceService.updateTaskStatus(taskId, body.status);
|
|
2045
|
+
const body = await c.req.json().catch(() => null);
|
|
2046
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
2047
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
2069
2048
|
}
|
|
2049
|
+
const fields = {};
|
|
2050
|
+
for (const key of [
|
|
2051
|
+
'title',
|
|
2052
|
+
'status',
|
|
2053
|
+
'isAcceptanceCriterion',
|
|
2054
|
+
'sortOrder',
|
|
2055
|
+
'afterTaskId',
|
|
2056
|
+
'verification',
|
|
2057
|
+
]) {
|
|
2058
|
+
if (body[key] !== undefined)
|
|
2059
|
+
Object.assign(fields, { [key]: body[key] });
|
|
2060
|
+
}
|
|
2061
|
+
if (Object.keys(fields).length === 0)
|
|
2062
|
+
return c.json({ error: 'At least one task field is required' }, 400);
|
|
2063
|
+
if (fields.title !== undefined && (typeof fields.title !== 'string' || !fields.title.trim())) {
|
|
2064
|
+
return c.json({ error: 'Title cannot be empty' }, 400);
|
|
2065
|
+
}
|
|
2066
|
+
if (fields.status !== undefined && !['pending', 'in_progress', 'done'].includes(fields.status)) {
|
|
2067
|
+
return c.json({ error: 'Invalid status. Must be one of: pending, in_progress, done' }, 400);
|
|
2068
|
+
}
|
|
2069
|
+
if (fields.title !== undefined)
|
|
2070
|
+
fields.title = fields.title.trim();
|
|
2071
|
+
workspaceService.updateTask(taskId, fields);
|
|
2070
2072
|
return c.json({ ok: true });
|
|
2071
2073
|
}
|
|
2072
2074
|
catch (err) {
|
|
2073
2075
|
const message = err instanceof Error ? err.message : String(err);
|
|
2074
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2076
|
+
return c.json({ error: message }, err instanceof TaskValidationError ? 400 : workspaceErrorStatus(err));
|
|
2075
2077
|
}
|
|
2076
2078
|
});
|
|
2077
2079
|
// DELETE /api/workspaces/:id/tasks/:taskId — delete a task
|
|
@@ -4302,104 +4304,12 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
4302
4304
|
}
|
|
4303
4305
|
});
|
|
4304
4306
|
// POST /api/workspaces/:id/start-review — ask the agent to review committed + uncommitted changes
|
|
4305
|
-
app.post('/:id/start-review', async (c) => {
|
|
4307
|
+
app.post('/:id/start-review', migrationGuard, async (c) => {
|
|
4306
4308
|
try {
|
|
4307
|
-
|
|
4308
|
-
const workspace = workspaceService.getWorkspace(id);
|
|
4309
|
-
if (!workspace) {
|
|
4310
|
-
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
4311
|
-
}
|
|
4312
|
-
const body = await c.req
|
|
4313
|
-
.json()
|
|
4314
|
-
.catch(() => ({}));
|
|
4315
|
-
const additionalInstructions = (body.additionalInstructions ?? '').trim();
|
|
4316
|
-
const newSession = body.newSession === true;
|
|
4317
|
-
const worktreePath = workspace.worktreePath;
|
|
4318
|
-
// Best-effort fetch so the base ref is fresh
|
|
4319
|
-
try {
|
|
4320
|
-
await execFileAsync('git', ['fetch', 'origin', workspace.sourceBranch], { cwd: worktreePath });
|
|
4321
|
-
}
|
|
4322
|
-
catch (err) {
|
|
4323
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4324
|
-
console.warn(`[start-review] git fetch origin ${workspace.sourceBranch} failed: ${msg}`);
|
|
4325
|
-
}
|
|
4326
|
-
// Resolve base commit
|
|
4327
|
-
let baseCommit;
|
|
4328
|
-
try {
|
|
4329
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${workspace.sourceBranch}`], {
|
|
4330
|
-
cwd: worktreePath,
|
|
4331
|
-
});
|
|
4332
|
-
baseCommit = stdout.trim();
|
|
4333
|
-
}
|
|
4334
|
-
catch (err) {
|
|
4335
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4336
|
-
return c.json({ error: `Cannot resolve base commit for branch ${workspace.sourceBranch}: ${msg}` }, 500);
|
|
4337
|
-
}
|
|
4338
|
-
// Build context
|
|
4339
|
-
const commits = gitOps.getCommitsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4340
|
-
const committedStats = gitOps.getDiffStatsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4341
|
-
const workingTreeStats = gitOps.getWorkingTreeDiffStats(worktreePath);
|
|
4342
|
-
const diffStats = workingTreeStats.trim().length > 0
|
|
4343
|
-
? `${committedStats}\n\n— Working tree (uncommitted) —\n${workingTreeStats}`
|
|
4344
|
-
: committedStats;
|
|
4345
|
-
const template = getActiveReviewTemplate();
|
|
4346
|
-
const rendered = renderReviewTemplate(template, {
|
|
4347
|
-
workspace,
|
|
4348
|
-
commits,
|
|
4349
|
-
diffStats,
|
|
4350
|
-
baseCommit,
|
|
4351
|
-
additionalInstructions,
|
|
4352
|
-
});
|
|
4353
|
-
let messageSent = false;
|
|
4354
|
-
let emitSessionId;
|
|
4355
|
-
if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
|
|
4356
|
-
return c.json({
|
|
4357
|
-
code: 'compacting',
|
|
4358
|
-
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
4359
|
-
}, 409);
|
|
4360
|
-
}
|
|
4361
|
-
if (newSession) {
|
|
4362
|
-
// Stop current agent, wait for it, then start fresh.
|
|
4363
|
-
try {
|
|
4364
|
-
assertAgentStopped(await agentManager.stopAgentAndWait(workspace.id, undefined, 'replacement'));
|
|
4365
|
-
}
|
|
4366
|
-
catch (err) {
|
|
4367
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4368
|
-
console.error(`[start-review] stopAgentAndWait failed (continuing): ${msg}`);
|
|
4369
|
-
throw err;
|
|
4370
|
-
}
|
|
4371
|
-
try {
|
|
4372
|
-
const agent = agentManager.startAgent(workspace.id, worktreePath, rendered, workspace.model, false /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
4373
|
-
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
4374
|
-
emitSessionId = agent?.agentSessionId;
|
|
4375
|
-
messageSent = true;
|
|
4376
|
-
}
|
|
4377
|
-
catch (err) {
|
|
4378
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4379
|
-
return c.json({ error: `Failed to start review session: ${msg}` }, 500);
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
else {
|
|
4383
|
-
const session = workspaceService.getActiveSession(workspace.id);
|
|
4384
|
-
emitSessionId = session?.id;
|
|
4385
|
-
try {
|
|
4386
|
-
const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
|
|
4387
|
-
emitSessionId = delivery.agentSessionId;
|
|
4388
|
-
messageSent = true;
|
|
4389
|
-
}
|
|
4390
|
-
catch (deliveryErr) {
|
|
4391
|
-
const message = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
|
|
4392
|
-
return c.json({ error: `Failed to dispatch review prompt: ${message}` }, 500);
|
|
4393
|
-
}
|
|
4394
|
-
}
|
|
4395
|
-
// Emit AFTER dispatch so the user:message lands in the correct session id —
|
|
4396
|
-
// for newSession=true that's the freshly created session, not the previous one.
|
|
4397
|
-
wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, emitSessionId);
|
|
4398
|
-
return c.json({ ok: true, messageSent, newSession });
|
|
4309
|
+
return c.json(await startWorkspaceReview(c.req.param('id'), await c.req.json().catch(() => ({}))));
|
|
4399
4310
|
}
|
|
4400
4311
|
catch (err) {
|
|
4401
|
-
|
|
4402
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4312
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, err instanceof ReviewRequestError ? err.status : workspaceErrorStatus(err));
|
|
4403
4313
|
}
|
|
4404
4314
|
});
|
|
4405
4315
|
// POST /api/workspaces/:id/start-ci-fix — dispatch the configured CI-fix
|
|
@@ -315,6 +315,7 @@ export function createClaudeCodeEngine() {
|
|
|
315
315
|
kind: 'error',
|
|
316
316
|
category: 'other',
|
|
317
317
|
message: 'Session force-ended by the liveness watchdog: no SDK activity within the deadline. If the agent was legitimately busy, this is a bug worth reporting.',
|
|
318
|
+
code: 'stream_idle_timeout',
|
|
318
319
|
});
|
|
319
320
|
emitSessionEnded('watchdog', null);
|
|
320
321
|
}
|
|
@@ -587,6 +588,7 @@ export function createClaudeCodeEngine() {
|
|
|
587
588
|
}
|
|
588
589
|
})();
|
|
589
590
|
const engineProcess = {
|
|
591
|
+
closed: iteratorPromise.then(() => { }),
|
|
590
592
|
get pid() {
|
|
591
593
|
return undefined;
|
|
592
594
|
},
|
|
@@ -124,14 +124,41 @@ export function createCodexEngine() {
|
|
|
124
124
|
});
|
|
125
125
|
void childFailurePromise.catch(() => { });
|
|
126
126
|
const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
|
|
127
|
+
let childExited = false;
|
|
128
|
+
let resolveChildExited;
|
|
129
|
+
const childExitedPromise = new Promise((resolve) => {
|
|
130
|
+
resolveChildExited = resolve;
|
|
131
|
+
});
|
|
132
|
+
const confirmChildExited = () => {
|
|
133
|
+
childExited = true;
|
|
134
|
+
resolveChildExited();
|
|
135
|
+
};
|
|
136
|
+
let shutdownPromise;
|
|
137
|
+
const terminateChild = () => {
|
|
138
|
+
if (childExited)
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
shutdownPromise ??= (async () => {
|
|
141
|
+
if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
142
|
+
console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
|
|
143
|
+
if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
144
|
+
throw new Error('Codex app-server did not exit after SIGKILL');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
confirmChildExited();
|
|
148
|
+
})();
|
|
149
|
+
return shutdownPromise;
|
|
150
|
+
};
|
|
127
151
|
child.on('error', (error) => {
|
|
128
152
|
const isExpectedAbort = abortController.signal.aborted && (error.code === 'ABORT_ERR' || error.name === 'AbortError');
|
|
129
153
|
if (isExpectedAbort)
|
|
130
154
|
return;
|
|
155
|
+
if (child.pid === undefined)
|
|
156
|
+
confirmChildExited();
|
|
131
157
|
console.error('[codex] child process error:', error);
|
|
132
158
|
rejectChildFailure(error);
|
|
133
159
|
});
|
|
134
160
|
child.once('exit', (code, signal) => {
|
|
161
|
+
confirmChildExited();
|
|
135
162
|
if (!iteratorRunning || abortController.signal.aborted)
|
|
136
163
|
return;
|
|
137
164
|
const detail = code === null ? `signal ${signal ?? 'unknown'}` : `code ${code}`;
|
|
@@ -154,6 +181,7 @@ export function createCodexEngine() {
|
|
|
154
181
|
let rejectTurnDone;
|
|
155
182
|
const activeSubagentThreads = new Map();
|
|
156
183
|
let waitingForBackgroundSubagents = false;
|
|
184
|
+
let watchdogEnded = false;
|
|
157
185
|
const turnDonePromise = new Promise((resolve, reject) => {
|
|
158
186
|
resolveTurnDone = resolve;
|
|
159
187
|
rejectTurnDone = reject;
|
|
@@ -169,6 +197,8 @@ export function createCodexEngine() {
|
|
|
169
197
|
// deadline tracks the last activity, not the start of the wait.
|
|
170
198
|
const armSubagentStallWatchdog = () => {
|
|
171
199
|
clearSubagentStallWatchdog();
|
|
200
|
+
if (!waitingForBackgroundSubagents || pendingByCallId.size > 0 || activeSubagentThreads.size === 0)
|
|
201
|
+
return;
|
|
172
202
|
subagentStallTimer = setTimeout(() => {
|
|
173
203
|
subagentStallTimer = undefined;
|
|
174
204
|
console.warn(`[codex-engine] Background subagent thread(s) still tracked active ${CODEX_SUBAGENT_STALL_TIMEOUT_MS}ms after the turn completed — forcing session drain.`);
|
|
@@ -176,7 +206,13 @@ export function createCodexEngine() {
|
|
|
176
206
|
// Forced, unclean termination (a thread may still be alive
|
|
177
207
|
// server-side) — never report it as a normal completion, so
|
|
178
208
|
// auto-loop doesn't treat an orphaned run as forward progress.
|
|
179
|
-
|
|
209
|
+
watchdogEnded = true;
|
|
210
|
+
safeEmit({
|
|
211
|
+
kind: 'error',
|
|
212
|
+
category: 'other',
|
|
213
|
+
code: 'subagent_idle_timeout',
|
|
214
|
+
message: 'Session force-ended: background subagents stopped reporting activity (watchdog).',
|
|
215
|
+
});
|
|
180
216
|
if (waitingForBackgroundSubagents)
|
|
181
217
|
resolveTurnDone();
|
|
182
218
|
}, CODEX_SUBAGENT_STALL_TIMEOUT_MS);
|
|
@@ -209,8 +245,13 @@ export function createCodexEngine() {
|
|
|
209
245
|
const turnLiveness = createTurnLiveness({
|
|
210
246
|
timeoutMs: CODEX_TURN_IDLE_TIMEOUT_MS,
|
|
211
247
|
onTimeout() {
|
|
212
|
-
|
|
213
|
-
safeEmit({
|
|
248
|
+
watchdogEnded = true;
|
|
249
|
+
safeEmit({
|
|
250
|
+
kind: 'error',
|
|
251
|
+
category: 'other',
|
|
252
|
+
code: 'stream_idle_timeout',
|
|
253
|
+
message: 'Codex stopped reporting activity for this turn',
|
|
254
|
+
});
|
|
214
255
|
rejectTurnDone(new CodexTurnTimeoutError());
|
|
215
256
|
},
|
|
216
257
|
});
|
|
@@ -221,6 +262,21 @@ export function createCodexEngine() {
|
|
|
221
262
|
rejectTurnDone(err);
|
|
222
263
|
});
|
|
223
264
|
const fileChanges = new Map();
|
|
265
|
+
const isParentTurn = (threadId, turnId) => (!threadId || threadId === discoveredSessionId) && (!activeTurnId || !turnId || turnId === activeTurnId);
|
|
266
|
+
const otherTurnMappers = new Map();
|
|
267
|
+
const mapTurnEvents = (notification, map) => {
|
|
268
|
+
if (isParentTurn(notification.threadId, notification.turnId))
|
|
269
|
+
return map(mapperState);
|
|
270
|
+
const key = JSON.stringify([notification.threadId, notification.turnId]);
|
|
271
|
+
let state = otherTurnMappers.get(key);
|
|
272
|
+
if (!state) {
|
|
273
|
+
state = createMapperState();
|
|
274
|
+
otherTurnMappers.set(key, state);
|
|
275
|
+
}
|
|
276
|
+
// Child output remains visible, but its failures/compaction/markers
|
|
277
|
+
// cannot control the parent session or its auto-loop lifecycle.
|
|
278
|
+
return map(state).filter((event) => event.kind !== 'error' && !event.kind.startsWith('session:'));
|
|
279
|
+
};
|
|
224
280
|
const itemKey = (threadId, turnId, itemId) => JSON.stringify([threadId, turnId, itemId]);
|
|
225
281
|
const client = createAppServerClient({
|
|
226
282
|
stdin: child.stdin,
|
|
@@ -228,6 +284,10 @@ export function createCodexEngine() {
|
|
|
228
284
|
clientInfo: { name: 'kobo', version: getPackageVersion() },
|
|
229
285
|
onNotification(method, params) {
|
|
230
286
|
turnLiveness.activity();
|
|
287
|
+
const notification = params;
|
|
288
|
+
if (notification?.threadId && activeSubagentThreads.has(notification.threadId)) {
|
|
289
|
+
armSubagentStallWatchdog();
|
|
290
|
+
}
|
|
231
291
|
// Ignored notifications — harmless bookkeeping by the server
|
|
232
292
|
if (method === 'mcpServer/startupStatus/updated') {
|
|
233
293
|
const status = (params ?? {});
|
|
@@ -257,7 +317,7 @@ export function createCodexEngine() {
|
|
|
257
317
|
const n = params;
|
|
258
318
|
if (n.item.type === 'fileChange')
|
|
259
319
|
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
260
|
-
const events = handleItemStarted(n.item,
|
|
320
|
+
const events = mapTurnEvents(n, (state) => handleItemStarted(n.item, state));
|
|
261
321
|
for (const ev of events)
|
|
262
322
|
safeEmit(ev);
|
|
263
323
|
if (n.item.type === 'collabAgentToolCall' && n.item.tool === 'spawnAgent') {
|
|
@@ -278,7 +338,7 @@ export function createCodexEngine() {
|
|
|
278
338
|
const n = params;
|
|
279
339
|
if (n.item.type === 'fileChange')
|
|
280
340
|
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
281
|
-
const events = handleItemCompleted(n.item,
|
|
341
|
+
const events = mapTurnEvents(n, (state) => handleItemCompleted(n.item, state));
|
|
282
342
|
for (const ev of events)
|
|
283
343
|
safeEmit(ev);
|
|
284
344
|
if (n.item.type === 'collabAgentToolCall') {
|
|
@@ -302,7 +362,7 @@ export function createCodexEngine() {
|
|
|
302
362
|
}
|
|
303
363
|
if (method === 'item/agentMessage/delta') {
|
|
304
364
|
const n = params;
|
|
305
|
-
for (const ev of handleAgentMessageDelta(n,
|
|
365
|
+
for (const ev of mapTurnEvents(n, (state) => handleAgentMessageDelta(n, state)))
|
|
306
366
|
safeEmit(ev);
|
|
307
367
|
return;
|
|
308
368
|
}
|
|
@@ -313,9 +373,9 @@ export function createCodexEngine() {
|
|
|
313
373
|
if (threadId === n.threadId && turnId === n.turn.id)
|
|
314
374
|
fileChanges.delete(key);
|
|
315
375
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
376
|
+
if (isParentTurn(n.threadId, n.turn.id)) {
|
|
377
|
+
for (const ev of handleTurnCompleted(n, mapperState))
|
|
378
|
+
safeEmit(ev);
|
|
319
379
|
if (n.turn?.status === 'completed' && activeSubagentThreads.size > 0) {
|
|
320
380
|
waitingForBackgroundSubagents = true;
|
|
321
381
|
turnLiveness.pause();
|
|
@@ -349,6 +409,8 @@ export function createCodexEngine() {
|
|
|
349
409
|
}
|
|
350
410
|
if (method === 'error') {
|
|
351
411
|
const n = params;
|
|
412
|
+
if (!isParentTurn(n.threadId, n.turnId))
|
|
413
|
+
return;
|
|
352
414
|
if (n.willRetry)
|
|
353
415
|
return;
|
|
354
416
|
const msg = n?.error?.message ?? 'unknown error';
|
|
@@ -371,6 +433,7 @@ export function createCodexEngine() {
|
|
|
371
433
|
register(callId, pending) {
|
|
372
434
|
pendingByCallId.set(callId, pending);
|
|
373
435
|
turnLiveness.pause();
|
|
436
|
+
clearSubagentStallWatchdog();
|
|
374
437
|
},
|
|
375
438
|
respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
|
|
376
439
|
respond: (reqId, result) => client.peer.respond(reqId, result),
|
|
@@ -444,7 +507,9 @@ export function createCodexEngine() {
|
|
|
444
507
|
? 'error'
|
|
445
508
|
: mapperState.sawTurnInterrupted
|
|
446
509
|
? 'killed'
|
|
447
|
-
:
|
|
510
|
+
: watchdogEnded
|
|
511
|
+
? 'watchdog'
|
|
512
|
+
: 'completed';
|
|
448
513
|
safeEmit({
|
|
449
514
|
kind: 'session:ended',
|
|
450
515
|
reason,
|
|
@@ -465,7 +530,11 @@ export function createCodexEngine() {
|
|
|
465
530
|
safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
|
|
466
531
|
}
|
|
467
532
|
else if (error.name === 'CodexTurnTimeoutError') {
|
|
468
|
-
safeEmit({
|
|
533
|
+
safeEmit({
|
|
534
|
+
kind: 'session:ended',
|
|
535
|
+
reason: mapperState.sawErrorResult ? 'error' : 'watchdog',
|
|
536
|
+
exitCode: null,
|
|
537
|
+
});
|
|
469
538
|
}
|
|
470
539
|
else if (QUOTA_PATTERN.test(message)) {
|
|
471
540
|
tryEmitQuota(mapperState, safeEmit, message);
|
|
@@ -497,18 +566,15 @@ export function createCodexEngine() {
|
|
|
497
566
|
}
|
|
498
567
|
}
|
|
499
568
|
fileChanges.clear();
|
|
569
|
+
otherTurnMappers.clear();
|
|
500
570
|
pendingByCallId.clear();
|
|
501
571
|
pendingToolCalls.clear();
|
|
502
572
|
client.close();
|
|
503
|
-
|
|
504
|
-
child.kill('SIGTERM');
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
// best-effort
|
|
508
|
-
}
|
|
573
|
+
await terminateChild().catch((error) => console.error('[codex] runtime shutdown unconfirmed:', error));
|
|
509
574
|
}
|
|
510
575
|
})();
|
|
511
576
|
const engineProcess = {
|
|
577
|
+
closed: Promise.all([iteratorPromise, childExitedPromise]).then(() => { }),
|
|
512
578
|
get pid() {
|
|
513
579
|
return child.pid;
|
|
514
580
|
},
|
|
@@ -516,7 +582,7 @@ export function createCodexEngine() {
|
|
|
516
582
|
return discoveredSessionId;
|
|
517
583
|
},
|
|
518
584
|
isAlive() {
|
|
519
|
-
return iteratorRunning;
|
|
585
|
+
return iteratorRunning || !childExited;
|
|
520
586
|
},
|
|
521
587
|
sendMessage(text) {
|
|
522
588
|
const steer = async () => {
|
|
@@ -581,17 +647,7 @@ export function createCodexEngine() {
|
|
|
581
647
|
catch {
|
|
582
648
|
// swallow
|
|
583
649
|
}
|
|
584
|
-
|
|
585
|
-
// guarantee: a wedged `codex app-server` that ignores it outlives the
|
|
586
|
-
// stop, the archive, the delete and Kōbō's own shutdown, holding the
|
|
587
|
-
// worktree and its share of the model quota, with nothing left in
|
|
588
|
-
// this process tracking it.
|
|
589
|
-
if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
590
|
-
console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
|
|
591
|
-
if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
592
|
-
throw new Error('Codex app-server did not exit after SIGKILL');
|
|
593
|
-
}
|
|
594
|
-
}
|
|
650
|
+
await terminateChild();
|
|
595
651
|
},
|
|
596
652
|
resolvePendingUserInput(callId, response) {
|
|
597
653
|
const pending = pendingByCallId.get(callId);
|
|
@@ -601,8 +657,12 @@ export function createCodexEngine() {
|
|
|
601
657
|
// Only resume the idle-timeout clock once every outstanding
|
|
602
658
|
// approval has been answered — a sibling request may still be
|
|
603
659
|
// waiting on a human decision.
|
|
604
|
-
if (pendingByCallId.size === 0)
|
|
605
|
-
|
|
660
|
+
if (pendingByCallId.size === 0) {
|
|
661
|
+
if (waitingForBackgroundSubagents)
|
|
662
|
+
armSubagentStallWatchdog();
|
|
663
|
+
else
|
|
664
|
+
turnLiveness.resume();
|
|
665
|
+
}
|
|
606
666
|
const result = buildResponseForResolve(pending, response);
|
|
607
667
|
client.peer.respond(pending.requestId, result);
|
|
608
668
|
return true;
|