@oar-sh/oar 0.9.10 → 0.9.11
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 +314 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +95 -22
- package/server/claude-worker/claude-session-process.mjs +89 -11
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +76 -11
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/db-schema.mjs +59 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +251 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/bootstrap.js +158 -32
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +136 -93
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +787 -22
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +40 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +80 -14
- package/server/public/app/store.js +96 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +30 -6
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-jump-bottom-view.mjs +194 -20
- package/server/public/app/transcript-jump-bottom.mjs +132 -21
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/index.html +386 -15
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +1 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +184 -65
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +771 -193
- package/server/server-runtime.mjs +316 -100
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/remote-relay-dispatcher.mjs +57 -5
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +227 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +3 -1
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -41,6 +41,7 @@ import { openAIReasoningEffortsForModel } from '../../shared/openai-reasoning.mj
|
|
|
41
41
|
import { DEFAULT_CLAUDE_REASONING_EFFORTS } from '../services/provider-reasoning-effort.mjs';
|
|
42
42
|
import { claudeBaseModelId, claudeLongContextModelId } from '../../shared/model-id.mjs';
|
|
43
43
|
import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
|
|
44
|
+
import { normalizeBackgroundTaskDigest } from '../../shared/worker-runtime/background-task-digest.mjs';
|
|
44
45
|
import { ANSWERED_ELSEWHERE_KIND, STEER_FOLDED_TEXT } from '../../shared/steer-settle-markers.mjs';
|
|
45
46
|
import { isLiveResend } from '../services/steer-resend.mjs';
|
|
46
47
|
import { resolveProviderModelSelection } from '../services/provider-model-selection.mjs';
|
|
@@ -57,9 +58,11 @@ import { RELAY_EVENT_MESSAGE_KIND } from '../../shared/relay-events.mjs';
|
|
|
57
58
|
import { isRelayShutdownQueued, relayShutdownGateReply } from '../services/relay-shutdown-gate.mjs';
|
|
58
59
|
import { createStaleAttemptLogLimiter } from '../services/stale-attempt-log-limiter.mjs';
|
|
59
60
|
import { createSubagentTimelinePublisher } from '../services/subagent-viewer-rooms.mjs';
|
|
61
|
+
import { createLocalRequestCheck } from '../services/request-boundary.mjs';
|
|
60
62
|
import { buildClaudeCloudDelivery, isClaudeCloudProviderType } from '../services/claude-cloud-session-service.mjs';
|
|
61
63
|
import {
|
|
62
64
|
admitRemoteRelayRequest,
|
|
65
|
+
readRemoteRelayRequest,
|
|
63
66
|
findMentionedRemoteRelays,
|
|
64
67
|
findOriginRemoteRelay,
|
|
65
68
|
formatRemoteRelayOriginHint,
|
|
@@ -1775,6 +1778,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1775
1778
|
getModelCatalogState,
|
|
1776
1779
|
buildRelayReadyBannerData,
|
|
1777
1780
|
backgroundTaskStore,
|
|
1781
|
+
backgroundWorkView = null,
|
|
1778
1782
|
sendWorkerControl,
|
|
1779
1783
|
ensureSessionId,
|
|
1780
1784
|
touchCli,
|
|
@@ -1811,6 +1815,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1811
1815
|
relayRestartOrchestrator,
|
|
1812
1816
|
requestRelayShutdown,
|
|
1813
1817
|
cancelRelayShutdown = null,
|
|
1818
|
+
// Truly local = loopback peer, no tunnel marker, no forwarding header
|
|
1819
|
+
// (services/request-boundary.mjs). The runtime passes the check built
|
|
1820
|
+
// with the configured marker headers.
|
|
1821
|
+
isTrulyLocalRequest = createLocalRequestCheck().isTrulyLocal,
|
|
1814
1822
|
featureFlags,
|
|
1815
1823
|
sessionWorkerRegistry,
|
|
1816
1824
|
sessionWorkerSupervisor,
|
|
@@ -1833,6 +1841,9 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1833
1841
|
// agent-sent messages, the inbound switch, and mention unlocks. Absent in
|
|
1834
1842
|
// older wiring and most tests, where every remote-relay step is a no-op.
|
|
1835
1843
|
remoteRelayInbound = null,
|
|
1844
|
+
// Session groups (repositories/conversation-group-repository.mjs): the
|
|
1845
|
+
// group stop route. Absent in older wiring and most tests.
|
|
1846
|
+
conversationGroups = null,
|
|
1836
1847
|
// Relay event subscriptions (services/relay-event-service.mjs) and the
|
|
1837
1848
|
// per-session activity they read (services/session-activity-tracker.mjs).
|
|
1838
1849
|
// Absent in older wiring and most tests: every hook is then a no-op.
|
|
@@ -1936,21 +1947,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1936
1947
|
|| event.includes('blocked');
|
|
1937
1948
|
}
|
|
1938
1949
|
|
|
1939
|
-
function isLoopbackAddress(value) {
|
|
1940
|
-
const text = String(value || '').trim().toLowerCase();
|
|
1941
|
-
if (!text) return false;
|
|
1942
|
-
return text === '127.0.0.1'
|
|
1943
|
-
|| text === '::1'
|
|
1944
|
-
|| text === '::ffff:127.0.0.1'
|
|
1945
|
-
|| text === '::ffff:7f00:1';
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
function isLoopbackRequest(req) {
|
|
1949
|
-
const socketAddress = req?.socket?.remoteAddress;
|
|
1950
|
-
const ipAddress = req?.ip;
|
|
1951
|
-
return isLoopbackAddress(socketAddress) || isLoopbackAddress(ipAddress);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
1950
|
function readBridgeIdentity(req) {
|
|
1955
1951
|
return relayBridgeOwnerService?.normalizeIdentity?.({
|
|
1956
1952
|
pid: req.headers['x-relay-process-pid'],
|
|
@@ -2966,36 +2962,54 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
2966
2962
|
if (!admitRemoteRelayRequest(req, res, remoteRelayInbound)) return;
|
|
2967
2963
|
const conversationId = String(req.params.conversationId || '').trim();
|
|
2968
2964
|
const requestedMessageId = String(req.body?.messageId || req.body?.queueMessageId || '').trim();
|
|
2965
|
+
const outcome = requestTurnStop(conversationId, {
|
|
2966
|
+
requestedMessageId,
|
|
2967
|
+
clientId: String(req.body?.clientId || '').trim() || null,
|
|
2968
|
+
});
|
|
2969
|
+
if (!outcome.ok) return rejectSessionBinding(res, outcome.status, outcome.error);
|
|
2970
|
+
return res.json(outcome.body);
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
/**
|
|
2974
|
+
* The work of POST /api/conversation/:id/cancel-turn: an `abort_turn`
|
|
2975
|
+
* control for the live turn. `{ ok: true, body }` with the route's answer,
|
|
2976
|
+
* or `{ ok: false, status, error }` when the conversation is unknown.
|
|
2977
|
+
*/
|
|
2978
|
+
function requestTurnStop(conversationId, { requestedMessageId = '', clientId = null } = {}) {
|
|
2969
2979
|
const sessionState = getConversationSessionState(conversationId);
|
|
2970
|
-
if (!sessionState.ok) {
|
|
2971
|
-
return rejectSessionBinding(res, sessionState.status, sessionState.error);
|
|
2972
|
-
}
|
|
2980
|
+
if (!sessionState.ok) return { ok: false, status: sessionState.status, error: sessionState.error };
|
|
2973
2981
|
|
|
2974
2982
|
// The live turn, not merely the newest processing row: a steered message
|
|
2975
2983
|
// folded into the running turn is also `processing`, and Stop must target
|
|
2976
2984
|
// the turn actually executing (which the worker interrupts as a whole).
|
|
2977
2985
|
const queueRow = resolveLiveTurnQueueRow(conversationId) || null;
|
|
2978
2986
|
if (!queueRow) {
|
|
2979
|
-
return
|
|
2987
|
+
return {
|
|
2980
2988
|
ok: true,
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2989
|
+
body: {
|
|
2990
|
+
ok: true,
|
|
2991
|
+
queued: false,
|
|
2992
|
+
acknowledgement: 'no-active-turn',
|
|
2993
|
+
requestedMessageId: requestedMessageId || null,
|
|
2994
|
+
activeMessageId: null,
|
|
2995
|
+
},
|
|
2996
|
+
};
|
|
2986
2997
|
}
|
|
2987
2998
|
|
|
2988
2999
|
const activeMessageId = String(queueRow.id || '').trim();
|
|
2989
3000
|
if (requestedMessageId && activeMessageId && requestedMessageId !== activeMessageId) {
|
|
2990
3001
|
const requestedRow = stmts.findQById.get(requestedMessageId) || null;
|
|
2991
3002
|
const requestedStatus = String(requestedRow?.status || '').trim().toLowerCase();
|
|
2992
|
-
return
|
|
3003
|
+
return {
|
|
2993
3004
|
ok: true,
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3005
|
+
body: {
|
|
3006
|
+
ok: true,
|
|
3007
|
+
queued: false,
|
|
3008
|
+
acknowledgement: requestedStatus === 'processing' ? 'message-mismatch' : 'already-finished',
|
|
3009
|
+
requestedMessageId,
|
|
3010
|
+
activeMessageId,
|
|
3011
|
+
},
|
|
3012
|
+
};
|
|
2999
3013
|
}
|
|
3000
3014
|
|
|
3001
3015
|
const sdkSessionId = normalizeSessionWorkerId(
|
|
@@ -3004,33 +3018,39 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
3004
3018
|
|| sessionState.conversationSdkSessionId,
|
|
3005
3019
|
);
|
|
3006
3020
|
if (!sdkSessionId) {
|
|
3007
|
-
return
|
|
3021
|
+
return {
|
|
3008
3022
|
ok: true,
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3023
|
+
body: {
|
|
3024
|
+
ok: true,
|
|
3025
|
+
queued: false,
|
|
3026
|
+
acknowledgement: 'active-turn-unbound',
|
|
3027
|
+
requestedMessageId: activeMessageId || requestedMessageId || null,
|
|
3028
|
+
activeMessageId,
|
|
3029
|
+
},
|
|
3030
|
+
};
|
|
3014
3031
|
}
|
|
3015
3032
|
|
|
3016
3033
|
const existing = findActiveRelayControlByQueueMessage.get(activeMessageId, 'abort_turn') || null;
|
|
3017
3034
|
if (existing) {
|
|
3018
|
-
return
|
|
3035
|
+
return {
|
|
3019
3036
|
ok: true,
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3037
|
+
body: {
|
|
3038
|
+
ok: true,
|
|
3039
|
+
queued: true,
|
|
3040
|
+
duplicate: true,
|
|
3041
|
+
acknowledgement: 'already-requested',
|
|
3042
|
+
requestedMessageId: activeMessageId || requestedMessageId || null,
|
|
3043
|
+
activeMessageId,
|
|
3044
|
+
control: formatRelayControlResponse(existing),
|
|
3045
|
+
},
|
|
3046
|
+
};
|
|
3027
3047
|
}
|
|
3028
3048
|
|
|
3029
3049
|
const now = new Date().toISOString();
|
|
3030
3050
|
const controlId = uuidv4();
|
|
3031
3051
|
const requestPayload = JSON.stringify({
|
|
3032
3052
|
source: 'relay-ui',
|
|
3033
|
-
requestedByClientId:
|
|
3053
|
+
requestedByClientId: clientId || null,
|
|
3034
3054
|
});
|
|
3035
3055
|
insertRelayControlRequest.run(
|
|
3036
3056
|
controlId,
|
|
@@ -3043,14 +3063,59 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
3043
3063
|
now,
|
|
3044
3064
|
);
|
|
3045
3065
|
const control = getRelayControlRequestById.get(controlId) || null;
|
|
3046
|
-
return
|
|
3066
|
+
return {
|
|
3047
3067
|
ok: true,
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3068
|
+
body: {
|
|
3069
|
+
ok: true,
|
|
3070
|
+
queued: true,
|
|
3071
|
+
acknowledgement: 'stop-queued',
|
|
3072
|
+
requestedMessageId: activeMessageId || requestedMessageId || null,
|
|
3073
|
+
activeMessageId,
|
|
3074
|
+
control: formatRelayControlResponse(control),
|
|
3075
|
+
},
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// POST /api/conversation-groups/:id/stop — "Stop all running" on a session
|
|
3080
|
+
// group's head row: a stop for every member's live turn, and a stop for
|
|
3081
|
+
// every stoppable background task. Tasks the panel shows no Stop button
|
|
3082
|
+
// for are listed as `unstoppable`. The owner's action only: a paired
|
|
3083
|
+
// relay's agent is refused.
|
|
3084
|
+
app.post('/api/conversation-groups/:id/stop', auth, (req, res) => {
|
|
3085
|
+
if (readRemoteRelayRequest(req, remoteRelayInbound).remote) {
|
|
3086
|
+
return res.status(403).json({ error: 'Session groups are managed by this relay\'s owner only.' });
|
|
3087
|
+
}
|
|
3088
|
+
const groupId = String(req.params.id || '').trim();
|
|
3089
|
+
const group = groupId ? conversationGroups?.getGroup?.(groupId) : null;
|
|
3090
|
+
if (!group) return res.status(404).json({ ok: false, error: 'Group not found' });
|
|
3091
|
+
const clientId = String(req.body?.clientId || '').trim() || null;
|
|
3092
|
+
const turns = [];
|
|
3093
|
+
const tasks = [];
|
|
3094
|
+
const unstoppable = [];
|
|
3095
|
+
for (const member of conversationGroups.membersOf(groupId)) {
|
|
3096
|
+
if (member.archived) continue;
|
|
3097
|
+
const outcome = requestTurnStop(member.id, { clientId });
|
|
3098
|
+
if (outcome.ok && outcome.body.queued === true) turns.push({ id: member.id, messageId: outcome.body.activeMessageId });
|
|
3099
|
+
const conversation = stmts.getConvAnyStatus?.get?.(member.id) || null;
|
|
3100
|
+
const sessionId = String(conversation?.sdk_session_id || member.id);
|
|
3101
|
+
const liveTasks = [...new Set([member.id, sessionId])].flatMap((key) => backgroundTaskStore?.get?.(key) || []);
|
|
3102
|
+
for (const task of liveTasks) {
|
|
3103
|
+
// The store holds what the publish route keeps: `taskId`.
|
|
3104
|
+
const taskId = String(task?.taskId || '').trim();
|
|
3105
|
+
if (!taskId) continue;
|
|
3106
|
+
if (task?.stoppable === false) {
|
|
3107
|
+
unstoppable.push({ id: member.id, taskId, reason: 'no-stop' });
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
const sent = sendWorkerControl?.(sessionId, { type: 'stop_background_task', taskId });
|
|
3111
|
+
if (sent) tasks.push({ id: member.id, taskId });
|
|
3112
|
+
else unstoppable.push({ id: member.id, taskId, reason: 'worker-not-connected' });
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
if (turns.length || tasks.length) {
|
|
3116
|
+
console.log(`[${ts()}] GROUP STOP ${groupId.slice(0, 12)} turns=${turns.length} tasks=${tasks.length}`);
|
|
3117
|
+
}
|
|
3118
|
+
return res.json({ ok: true, turns, tasks, unstoppable });
|
|
3054
3119
|
});
|
|
3055
3120
|
|
|
3056
3121
|
app.post('/api/conversation/:conversationId/subagent/:subagentRunId/cancel', auth, (req, res) => {
|
|
@@ -5313,6 +5378,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5313
5378
|
const requesterSessionId = normalizeSessionWorkerId(requester?.sessionId);
|
|
5314
5379
|
const settleFailedHandled = [];
|
|
5315
5380
|
const settleFailedSkipped = [];
|
|
5381
|
+
let resyncBackgroundTasks = false;
|
|
5316
5382
|
const activeQueueMessageId = String(req.body?.activeQueueMessageId || '').trim();
|
|
5317
5383
|
// A persistent-process worker can hold several live rows at once (the
|
|
5318
5384
|
// running turn plus a delivered message queued behind it plus a
|
|
@@ -5379,6 +5445,32 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5379
5445
|
}
|
|
5380
5446
|
}
|
|
5381
5447
|
sessionWorkerSupervisor?.noteSessionHeartbeat?.(requesterSessionId);
|
|
5448
|
+
// The worker's digest of its live background-task ids against the set
|
|
5449
|
+
// held for it (keyed as the worker posts, by its session id). They
|
|
5450
|
+
// differ after a relay restart, which starts with no sets, and after a
|
|
5451
|
+
// lost post either way round: the answer then asks for the full set
|
|
5452
|
+
// again. A worker that sends no digest has no opinion and is never
|
|
5453
|
+
// asked; a relay that holds a set while the worker reports none asks
|
|
5454
|
+
// too, and the empty answer ends those tasks.
|
|
5455
|
+
// The process this beat comes from holds the session: a set post of
|
|
5456
|
+
// another process is a late one (see the task route).
|
|
5457
|
+
backgroundTaskStore?.noteHolder?.(requesterSessionId, normalizedRequesterPid);
|
|
5458
|
+
const taskDigest = normalizeBackgroundTaskDigest(req.body?.backgroundTasks);
|
|
5459
|
+
if (taskDigest && typeof backgroundTaskStore?.matches === 'function') {
|
|
5460
|
+
resyncBackgroundTasks = !backgroundTaskStore.matches(taskDigest, requesterSessionId, requesterConversationId);
|
|
5461
|
+
}
|
|
5462
|
+
// The placeholder for background work this runtime was never told the
|
|
5463
|
+
// details of (a worker that cannot resync yet): the pages hear when it
|
|
5464
|
+
// appears and when it goes without a set having replaced it.
|
|
5465
|
+
if (!resyncBackgroundTasks && backgroundWorkView) {
|
|
5466
|
+
const unreported = backgroundWorkView.hasUnreportedWork(requesterSessionId) === true;
|
|
5467
|
+
if (backgroundTaskStore?.announce?.(requesterSessionId, unreported)) {
|
|
5468
|
+
io.emit('background_tasks', {
|
|
5469
|
+
conversationId: requesterSessionId,
|
|
5470
|
+
tasks: backgroundWorkView.panelTasks(requesterSessionId, requesterConversationId),
|
|
5471
|
+
});
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5382
5474
|
// Consumed steers whose settle the worker could not save within its
|
|
5383
5475
|
// retry budget (e.g. its writes keep failing): the relay fails them
|
|
5384
5476
|
// itself with the worker's terminal error, never requeueing them.
|
|
@@ -5438,6 +5530,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5438
5530
|
pendingCount,
|
|
5439
5531
|
...(settleFailedHandled.length ? { settleFailedHandled } : {}),
|
|
5440
5532
|
...(settleFailedSkipped.length ? { settleFailedSkipped } : {}),
|
|
5533
|
+
...(resyncBackgroundTasks ? { resyncBackgroundTasks: true } : {}),
|
|
5441
5534
|
});
|
|
5442
5535
|
});
|
|
5443
5536
|
|
|
@@ -6421,10 +6514,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6421
6514
|
return res.json({ ok: true, recovered: rows.length, maxAgeMs });
|
|
6422
6515
|
});
|
|
6423
6516
|
|
|
6517
|
+
// Emptying the queue, restarting the relay and withdrawing a restart are
|
|
6518
|
+
// the user's own maintenance actions: the browser app calls them from a
|
|
6519
|
+
// phone, over the tunnel and over the LAN. The token is the boundary.
|
|
6424
6520
|
app.post('/api/queue/empty', auth, (req, res) => {
|
|
6425
|
-
if (!isLoopbackRequest(req)) {
|
|
6426
|
-
return res.status(403).json({ error: 'Queue empty endpoint is localhost-only' });
|
|
6427
|
-
}
|
|
6428
6521
|
const rows = stmts.listQueueForPauseDrop.all();
|
|
6429
6522
|
const droppedCount = rows.length;
|
|
6430
6523
|
if (!droppedCount) {
|
|
@@ -6452,9 +6545,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6452
6545
|
});
|
|
6453
6546
|
|
|
6454
6547
|
app.post('/api/relay/shutdown', auth, (req, res) => {
|
|
6455
|
-
if (!isLoopbackRequest(req)) {
|
|
6456
|
-
return res.status(403).json({ error: 'Relay shutdown endpoint is localhost-only' });
|
|
6457
|
-
}
|
|
6458
6548
|
if (typeof requestRelayShutdown !== 'function') {
|
|
6459
6549
|
return res.status(501).json({ error: 'Relay shutdown orchestration is unavailable' });
|
|
6460
6550
|
}
|
|
@@ -6472,9 +6562,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6472
6562
|
// Withdraw a queued (not yet started) shutdown/restart. Same trust boundary
|
|
6473
6563
|
// as queueing one.
|
|
6474
6564
|
app.post('/api/relay/shutdown/cancel', auth, (req, res) => {
|
|
6475
|
-
if (!isLoopbackRequest(req)) {
|
|
6476
|
-
return res.status(403).json({ error: 'Relay shutdown endpoint is localhost-only' });
|
|
6477
|
-
}
|
|
6478
6565
|
if (typeof cancelRelayShutdown !== 'function') {
|
|
6479
6566
|
return res.status(501).json({ error: 'Relay shutdown orchestration is unavailable' });
|
|
6480
6567
|
}
|
|
@@ -6483,9 +6570,11 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6483
6570
|
return res.json({ ok: true, ...result });
|
|
6484
6571
|
});
|
|
6485
6572
|
|
|
6573
|
+
// Called by the relay's own worker on this machine; nothing that came in
|
|
6574
|
+
// through a proxy or the tunnel may start a provider image request.
|
|
6486
6575
|
app.post('/api/image-operations/:operationId/execute', auth, async (req, res) => {
|
|
6487
|
-
if (!
|
|
6488
|
-
return res.status(403).json({ error: 'Image operation execution is
|
|
6576
|
+
if (!isTrulyLocalRequest(req)) {
|
|
6577
|
+
return res.status(403).json({ error: 'Image operation execution is reachable from this machine only' });
|
|
6489
6578
|
}
|
|
6490
6579
|
if (featureFlags?.IMAGE_CONVERSATION_CONTINUITY_ENABLED !== true) {
|
|
6491
6580
|
return res.status(404).json({ error: 'Image continuity is not enabled' });
|
|
@@ -7560,12 +7649,21 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7560
7649
|
const taskId = String(req.body?.taskId || '').trim();
|
|
7561
7650
|
if (!conversationId || !taskId) return res.status(400).json({ error: 'Missing conversationId or taskId' });
|
|
7562
7651
|
const exitCode = Number(req.body?.exitCode);
|
|
7652
|
+
const settledDescription = String(req.body?.description || '').trim().slice(0, 500);
|
|
7563
7653
|
sessionActivityTracker?.noteTaskSettled?.(conversationId, {
|
|
7564
7654
|
taskId,
|
|
7565
7655
|
toolUseId: sanitizeSubagentRunId(req.body?.toolUseId) || null,
|
|
7566
7656
|
status: String(req.body?.status || '').trim().slice(0, 40) || 'completed',
|
|
7567
7657
|
summary: String(req.body?.summary || '').trim().slice(0, 500) || null,
|
|
7568
7658
|
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
7659
|
+
// What the task was, from a worker that sends it: the end of a task
|
|
7660
|
+
// this runtime never knew (it ended while the relay was away) is
|
|
7661
|
+
// still an event then.
|
|
7662
|
+
...(settledDescription ? {
|
|
7663
|
+
description: settledDescription,
|
|
7664
|
+
taskType: String(req.body?.taskType || '').trim().slice(0, 80),
|
|
7665
|
+
startedAt: Number(req.body?.startedAt) || null,
|
|
7666
|
+
} : {}),
|
|
7569
7667
|
});
|
|
7570
7668
|
relayEventService?.afterStateChange?.(conversationId);
|
|
7571
7669
|
res.json({ ok: true });
|
|
@@ -7574,6 +7672,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7574
7672
|
// POST /api/background-tasks — a session worker publishes its live
|
|
7575
7673
|
// background-task set (REPLACE semantics, mirroring the SDK's
|
|
7576
7674
|
// background_tasks_changed signal) so the composer panel tracks it live.
|
|
7675
|
+
// `resync: true` marks the set as the answer to a heartbeat that asked for
|
|
7676
|
+
// it again (`resyncBackgroundTasks`): the same replace, and the pages get
|
|
7677
|
+
// the set again, but a task the relay had forgotten is not a new start.
|
|
7678
|
+
// `order` lets the relay drop a set that a newer one overtook.
|
|
7577
7679
|
app.post('/api/background-tasks', auth, (req, res) => {
|
|
7578
7680
|
touchCli();
|
|
7579
7681
|
const conversationId = String(req.body?.conversationId || '').trim();
|
|
@@ -7602,13 +7704,30 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7602
7704
|
toolUseId: sanitizeSubagentRunId(task?.toolUseId) || undefined,
|
|
7603
7705
|
}))
|
|
7604
7706
|
.filter((task) => task.taskId);
|
|
7707
|
+
const resync = req.body?.resync === true;
|
|
7708
|
+
// Posts can overtake each other on the way (an ordinary publish and the
|
|
7709
|
+
// answer to a resync, two publishes): a worker numbers its sets
|
|
7710
|
+
// (`order: { epoch, seq }`), and one older than the newest applied is
|
|
7711
|
+
// dropped, or a task of the newer set would read as ended. A worker
|
|
7712
|
+
// that numbers none is taken as it comes.
|
|
7713
|
+
// A drop is never silent: it is logged, and the answer says `stale`,
|
|
7714
|
+
// on which the worker asks for a heartbeat (which makes it the holder
|
|
7715
|
+
// of the session if it was not yet, and has the relay ask for the set).
|
|
7716
|
+
const setOrder = backgroundTaskStore?.acceptOrder?.(conversationId, req.body?.order, {
|
|
7717
|
+
pid: readBridgeIdentity(req)?.pid,
|
|
7718
|
+
});
|
|
7719
|
+
if (setOrder && setOrder.accepted === false) {
|
|
7720
|
+
console.log(`[${typeof ts === 'function' ? ts() : new Date().toISOString()}] BG-TASKS set dropped (${setOrder.reason}) session=${conversationId.slice(0, 8)} epoch=${Number(req.body?.order?.epoch) || 0} seq=${Number(req.body?.order?.seq) || 0} tasks=${tasks.length}`);
|
|
7721
|
+
return res.json({ ok: true, count: tasks.length, stale: true, reason: setOrder.reason });
|
|
7722
|
+
}
|
|
7605
7723
|
backgroundTaskStore?.replace?.(conversationId, tasks);
|
|
7724
|
+
backgroundTaskStore?.announce?.(conversationId, false);
|
|
7606
7725
|
io.emit('background_tasks', { conversationId, tasks });
|
|
7607
7726
|
// The store is keyed as the worker posts (its SDK session id); the
|
|
7608
7727
|
// tracker by the conversation.
|
|
7609
7728
|
try {
|
|
7610
7729
|
const trackedConversationId = resolveTaskConversationId(conversationId);
|
|
7611
|
-
sessionActivityTracker?.noteTaskSet?.(trackedConversationId, tasks);
|
|
7730
|
+
sessionActivityTracker?.noteTaskSet?.(trackedConversationId, tasks, { resync });
|
|
7612
7731
|
relayEventService?.afterStateChange?.(trackedConversationId);
|
|
7613
7732
|
} catch {}
|
|
7614
7733
|
// The panel's copy of a task lives in memory and goes when the task
|
|
@@ -7631,7 +7750,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7631
7750
|
.some((column) => before[column] !== after[column]);
|
|
7632
7751
|
if (changed) emitSubagentStatus(after);
|
|
7633
7752
|
}
|
|
7634
|
-
res.json({ ok: true, count: tasks.length });
|
|
7753
|
+
res.json({ ok: true, count: tasks.length, ...(resync ? { resync: true } : {}) });
|
|
7635
7754
|
});
|
|
7636
7755
|
|
|
7637
7756
|
// POST /api/conversation/:conversationId/background-task/:taskId/stop —
|
|
@@ -95,7 +95,15 @@ export function registerPreviewRoutes(app, deps) {
|
|
|
95
95
|
// Probe immediately so the card does not sit badge-less until the next
|
|
96
96
|
// scheduled sweep — and so a typo'd port shows as offline right away.
|
|
97
97
|
previewHealthProbe?.probeNow?.(created.preview.token);
|
|
98
|
-
|
|
98
|
+
// `existing`: this conversation already had a preview of the same target,
|
|
99
|
+
// and this is it (same token, same link, label unchanged).
|
|
100
|
+
res.json({
|
|
101
|
+
ok: true,
|
|
102
|
+
existing: created.existing === true,
|
|
103
|
+
preview: created.preview,
|
|
104
|
+
url: created.preview.url,
|
|
105
|
+
basePath: created.preview.basePath,
|
|
106
|
+
});
|
|
99
107
|
});
|
|
100
108
|
|
|
101
109
|
// DELETE /api/previews/:token — the link stops resolving immediately. The dev
|