@jini-ai/daemon 0.6.15-rc.2 → 0.6.16
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 +12 -0
- package/README.md +14 -4
- package/dist/agent-executor/codex/home-config.js +3 -2
- package/dist/agent-executor/codex/home-staging.js +2 -2
- package/dist/agent-executor/codex/isolation.js +4 -3
- package/dist/agent-executor/contracts.d.ts +1 -1
- package/dist/agent-executor/contracts.js +0 -1
- package/dist/agent-executor/event-translation.d.ts +3 -3
- package/dist/agent-executor/event-translation.js +6 -6
- package/dist/agent-executor/events.js +65 -51
- package/dist/agent-executor/index.d.ts +5 -5
- package/dist/agent-executor/index.js +5 -5
- package/dist/agent-executor/launch.js +30 -46
- package/dist/agent-executor/live-session-state.js +2 -2
- package/dist/agent-executor/mcp-bridge.js +5 -0
- package/dist/agent-executor/message-relay.js +5 -6
- package/dist/agent-executor/run-output.js +7 -2
- package/dist/ai-trace-lifecycle.d.ts +1 -1
- package/dist/ai-trace-lifecycle.js +1 -1
- package/dist/close-status.js +6 -2
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/read-only-tools.js +5 -1
- package/dist/core/tool-audit.js +11 -7
- package/dist/core/tool-gaps.js +1 -1
- package/dist/delegated-result-correlation.js +2 -2
- package/dist/frontend-capability-tools.d.ts +2 -0
- package/dist/frontend-capability-tools.js +7 -1
- package/dist/frontend-session-registry.d.ts +42 -3
- package/dist/frontend-session-registry.js +61 -9
- package/dist/http/agents.d.ts +4 -0
- package/dist/http/attachments.js +13 -12
- package/dist/http/component-catalog.js +1 -1
- package/dist/http/connectors.js +7 -5
- package/dist/http/db-ops.js +5 -0
- package/dist/http/delegated-tools.js +5 -0
- package/dist/http/frontend-control.d.ts +13 -0
- package/dist/http/frontend-control.js +14 -3
- package/dist/http/frontend-guest-scope.d.ts +32 -0
- package/dist/http/frontend-guest-scope.js +63 -0
- package/dist/http/frontend-sessions.d.ts +29 -6
- package/dist/http/frontend-sessions.js +108 -28
- package/dist/http/host-tools.js +7 -5
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/memory.js +3 -1
- package/dist/http/model-proxy.js +13 -8
- package/dist/http/research-tavily.adapter.js +2 -1
- package/dist/http/routines.js +7 -5
- package/dist/http/terminals.js +5 -0
- package/dist/http/tool-catalog.js +1 -1
- package/dist/http/xai.js +10 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/legacy-data-migration.d.ts +0 -12
- package/dist/legacy-data-migration.js +21 -13
- package/dist/remote-tool-executor.js +2 -1
- package/dist/routines/schedule.js +2 -1
- package/dist/routines/scheduler.js +6 -7
- package/dist/run-lifecycle.js +2 -1
- package/dist/store/agent-sessions/sql.js +7 -3
- package/dist/store/event-log/sqlite-kernel.js +7 -2
- package/dist/store/event-log/sqlite.js +6 -2
- package/dist/surface-exchanges.js +3 -3
- package/dist/terminal-session.d.ts +51 -20
- package/dist/terminal-session.js +10 -94
- package/dist/terminal.adapter.d.ts +22 -0
- package/dist/terminal.adapter.js +47 -0
- package/dist/tool-executor.d.ts +4 -2
- package/dist/tool-executor.js +95 -15
- package/dist/tool-recovery/recovery.js +5 -3
- package/dist/tool-recovery/redaction.js +1 -2
- package/dist/tool-recovery/stack.js +5 -1
- package/dist/tool-result-media.d.ts +4 -0
- package/dist/tool-result-media.js +10 -3
- package/dist/tool-result-surfaces.js +10 -9
- package/package.json +30 -34
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { randomUUID } from 'node:crypto';
|
|
32
32
|
import { createApiError } from '@jini-ai/protocol';
|
|
33
|
+
import { FrontendSessionCapacityError } from '../frontend-session-registry.js';
|
|
33
34
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
34
35
|
import { guardSameOrigin } from '@jini-ai/http-kit';
|
|
35
36
|
import { createSseResponse } from '@jini-ai/http-kit';
|
|
@@ -59,7 +60,8 @@ function reportInternalError(deps, source, error) {
|
|
|
59
60
|
* from a bracketed query string) is rejected rather than coerced.
|
|
60
61
|
*/
|
|
61
62
|
export function parseCapabilityQuery({ raw }, _optional = {}) {
|
|
62
|
-
const
|
|
63
|
+
const listed = Array.isArray(raw) ? raw : [raw];
|
|
64
|
+
const values = raw === undefined ? [] : listed;
|
|
63
65
|
const capabilities = [];
|
|
64
66
|
for (const value of values) {
|
|
65
67
|
if (typeof value !== 'string' || value.trim().length === 0)
|
|
@@ -71,20 +73,34 @@ export function parseCapabilityQuery({ raw }, _optional = {}) {
|
|
|
71
73
|
/**
|
|
72
74
|
* Handles one surface's stream from open to close.
|
|
73
75
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* left. Same precedent as `run-stream.ts`.
|
|
76
|
+
* Invalid capability queries use a final SSE error event. Capacity admission happens before
|
|
77
|
+
* opening the stream so resource refusals retain a real HTTP status and create no connection.
|
|
77
78
|
*/
|
|
78
|
-
export function handleFrontendSessionStream({ req, res, deps }, _optional = {}) {
|
|
79
|
-
const
|
|
79
|
+
export function handleFrontendSessionStream({ req, res, deps, owner }, _optional = {}) {
|
|
80
|
+
const requested = parseCapabilityQuery({ raw: req.query['capability'] });
|
|
81
|
+
const capabilities = requested?.filter(id => !deps.allowedCapabilities || deps.allowedCapabilities.includes(id)) ?? null;
|
|
80
82
|
// Assigned below, after the connection exists — `attach` needs something to deliver through, and
|
|
81
83
|
// `onClose` has to be armed before then so a connection that dies during setup still detaches.
|
|
82
84
|
let handle;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
let expiry;
|
|
86
|
+
// Reserve capacity before committing SSE headers, so a full scope gets a real 429.
|
|
87
|
+
// Delivery cannot occur until the attached token has been sent to the client.
|
|
88
|
+
let connection;
|
|
89
|
+
if (capabilities !== null) {
|
|
90
|
+
handle = attachFrontendSession({ req, deps, capabilities, owner,
|
|
91
|
+
deliver: invocation => connection.send({ data: { type: 'invocation', ...invocation } }),
|
|
92
|
+
cancel: ({ invocationId }) => connection.send({ data: { type: 'cancelled', invocationId } }) });
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
connection = createSseResponse({ req, res }, {
|
|
96
|
+
// Close and expiry release the same quota reservation, without a separate counter lifecycle.
|
|
97
|
+
onClose: () => { clearTimeout(expiry); handle?.detach({}); },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
handle?.detach({});
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
88
104
|
if (capabilities === null) {
|
|
89
105
|
connection.send({
|
|
90
106
|
data: {
|
|
@@ -95,16 +111,22 @@ export function handleFrontendSessionStream({ req, res, deps }, _optional = {})
|
|
|
95
111
|
connection.close();
|
|
96
112
|
return;
|
|
97
113
|
}
|
|
98
|
-
|
|
99
|
-
handle = deps.registry.attach({ descriptor: { sessionId, capabilities }, deliver: (invocation) => connection.send({ data: { type: 'invocation', ...invocation } }) });
|
|
114
|
+
expiry = expireScopedSession({ owner, handle: handle, close: () => connection.close(), ttl: deps.sessionTtlMs });
|
|
100
115
|
connection.send({
|
|
101
116
|
data: {
|
|
102
117
|
type: 'attached',
|
|
103
|
-
sessionId,
|
|
118
|
+
sessionId: handle.sessionId,
|
|
104
119
|
bindToken: handle.bindToken,
|
|
105
120
|
}
|
|
106
121
|
});
|
|
107
122
|
}
|
|
123
|
+
/** Reserve the registry's capacity before SSE commits headers. Host keys are never client input. */
|
|
124
|
+
function attachFrontendSession({ req, deps, capabilities, owner, deliver, cancel }) {
|
|
125
|
+
const sessionId = (deps.newSessionId ?? randomUUID)();
|
|
126
|
+
const capacityKeys = deps.resolveCapacityKeys?.({ req, ...(owner ? { owner } : {}) });
|
|
127
|
+
return deps.registry.attach({ descriptor: { sessionId, capabilities, ...(owner ? { owner } : {}), ...(capacityKeys ? { capacityKeys } : {}) },
|
|
128
|
+
deliver, cancel });
|
|
129
|
+
}
|
|
108
130
|
function isRecord(value) {
|
|
109
131
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
110
132
|
}
|
|
@@ -119,12 +141,28 @@ function parseFrontendSessionResponse(input) {
|
|
|
119
141
|
if (typeof invocationId !== 'string' || invocationId.trim().length === 0) {
|
|
120
142
|
return err({ error: validationError({ message: 'invocationId must be a non-empty string' }, { issues: [{ path: 'invocationId', message: 'required non-empty string' }] }) });
|
|
121
143
|
}
|
|
122
|
-
|
|
144
|
+
return parseResponseOutcome({ sessionId, invocationId, body: input.body });
|
|
145
|
+
}
|
|
146
|
+
/** Progress is scoped to a pending invocation; only fixed, value-free decisions enter the audit. */
|
|
147
|
+
function parseApprovalProgress({ sessionId, invocationId, phase, decision }, _options = {}) {
|
|
148
|
+
if (decision === undefined)
|
|
149
|
+
return ok({ value: { sessionId, invocationId, phase } });
|
|
150
|
+
if (phase !== 'execution-started' || typeof decision !== 'string' || !['approved', 'declined', 'expired', 'abandoned'].includes(decision)) {
|
|
151
|
+
return err({ error: validationError({ message: 'invalid approval decision' }) });
|
|
152
|
+
}
|
|
153
|
+
return ok({ value: { sessionId, invocationId, phase, decision: decision } });
|
|
154
|
+
}
|
|
155
|
+
/** Identity parsing precedes outcome parsing so progress and results share exactly one scope. */
|
|
156
|
+
function parseResponseOutcome({ sessionId, invocationId, body }, _options = {}) {
|
|
157
|
+
const phase = body['phase'];
|
|
158
|
+
if (phase === 'approval-pending' || phase === 'execution-started')
|
|
159
|
+
return parseApprovalProgress({ sessionId, invocationId, phase, decision: body['decision'] });
|
|
160
|
+
const okFlag = body['ok'];
|
|
123
161
|
if (typeof okFlag !== 'boolean') {
|
|
124
162
|
return err({ error: validationError({ message: 'ok must be a boolean' }, { issues: [{ path: 'ok', message: 'required boolean' }] }) });
|
|
125
163
|
}
|
|
126
164
|
if (!okFlag) {
|
|
127
|
-
const message =
|
|
165
|
+
const message = body['message'];
|
|
128
166
|
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
129
167
|
// A failure with no reason gives the agent nothing to act on, which is how a caller ends up
|
|
130
168
|
// retrying the same refusal forever.
|
|
@@ -132,7 +170,7 @@ function parseFrontendSessionResponse(input) {
|
|
|
132
170
|
}
|
|
133
171
|
return ok({ value: { sessionId, invocationId, ok: false, message } });
|
|
134
172
|
}
|
|
135
|
-
return ok({ value: { sessionId, invocationId, ok: true, output:
|
|
173
|
+
return ok({ value: { sessionId, invocationId, ok: true, output: body['output'] } });
|
|
136
174
|
}
|
|
137
175
|
/**
|
|
138
176
|
* `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
|
|
@@ -141,6 +179,8 @@ function parseFrontendSessionResponse(input) {
|
|
|
141
179
|
*/
|
|
142
180
|
export const frontendSessionResponseRoute = defineJsonRoute({
|
|
143
181
|
method: 'post', path: FRONTEND_SESSION_RESPONSE_ROUTE_PATH, parse: parseFrontendSessionResponse, handle: ({ input, deps }) => {
|
|
182
|
+
if ('phase' in input)
|
|
183
|
+
return ok({ value: { settled: deps.registry.progress({ sessionId: input.sessionId, invocationId: input.invocationId, pending: input.phase === 'approval-pending', ...(input.decision ? { decision: input.decision } : {}) }) } });
|
|
144
184
|
const settled = deps.registry.settle({ sessionId: input.sessionId, invocationId: input.invocationId, outcome: input.ok
|
|
145
185
|
? { ok: true, output: input.output }
|
|
146
186
|
: { ok: false, message: input.message } });
|
|
@@ -156,25 +196,65 @@ export const frontendSessionResponseRoute = defineJsonRoute({
|
|
|
156
196
|
* never gets a stream (and, notably, never mints a session id or `bindToken` either).
|
|
157
197
|
*/
|
|
158
198
|
export function registerFrontendSessionRoutes({ app, deps, adapter }, _optional = {}) {
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
const prefix = deps.routePrefix ?? '/api/frontend-sessions';
|
|
200
|
+
app.get(`${prefix}/stream`, async (req, res) => {
|
|
201
|
+
// Scoped hosts authenticate origin together with guest/tab ownership in resolveOwner.
|
|
202
|
+
const origin = deps.resolveOwner ? ok({ value: undefined }) : guardSameOrigin({ req, origin: adapter });
|
|
161
203
|
if (!origin.ok) {
|
|
162
204
|
sendApiError({ res, status: statusForError({ error: origin.error }), error: origin.error });
|
|
163
205
|
return;
|
|
164
206
|
}
|
|
165
207
|
try {
|
|
166
|
-
|
|
208
|
+
const owner = deps.resolveOwner ? await deps.resolveOwner({ req, res }) : undefined;
|
|
209
|
+
if (owner === null) {
|
|
210
|
+
res.sendStatus(403);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
handleFrontendSessionStream({ req, res, deps, ...(owner ? { owner } : {}) });
|
|
167
214
|
}
|
|
168
215
|
catch (error) {
|
|
169
|
-
|
|
170
|
-
if (!res.headersSent) {
|
|
171
|
-
res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
172
|
-
}
|
|
173
|
-
else if (!res.writableEnded) {
|
|
174
|
-
res.end();
|
|
175
|
-
}
|
|
216
|
+
endStreamOnError({ error, res, deps });
|
|
176
217
|
}
|
|
177
218
|
});
|
|
178
|
-
|
|
219
|
+
const responsePath = `${prefix}/:sessionId/responses`;
|
|
220
|
+
if (deps.resolveOwner)
|
|
221
|
+
app.post(responsePath, async (req, res, next) => {
|
|
222
|
+
try {
|
|
223
|
+
const owner = await deps.resolveOwner({ req, res });
|
|
224
|
+
const bindToken = req.get('x-frontend-bind-token') ?? '';
|
|
225
|
+
if (!owner || !deps.registry.ownsSession({ sessionId: String(req.params['sessionId']), owner, bindToken })) {
|
|
226
|
+
res.sendStatus(403);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
res.sendStatus(403);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
mountJsonRoute({ app, spec: { ...frontendSessionResponseRoute, path: responsePath, requireSameOrigin: !deps.resolveOwner }, deps, adapter });
|
|
236
|
+
}
|
|
237
|
+
/** Capacity is ordinary admission refusal, not a loggable internal failure. */
|
|
238
|
+
function endStreamOnError({ error, res, deps }) {
|
|
239
|
+
if (error instanceof FrontendSessionCapacityError && !res.headersSent) {
|
|
240
|
+
res.sendStatus(429);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
reportInternalError(deps, 'stream', error);
|
|
244
|
+
if (res.headersSent) {
|
|
245
|
+
if (!res.writableEnded)
|
|
246
|
+
res.end();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Expiry tears down addressing and the stream together, including clients which never disconnect. */
|
|
253
|
+
function expireScopedSession({ owner, handle, close, ttl = 30 * 60_000 }) {
|
|
254
|
+
if (!owner)
|
|
255
|
+
return undefined;
|
|
256
|
+
const timer = setTimeout(() => { handle.detach({}); close(); }, ttl);
|
|
257
|
+
timer.unref();
|
|
258
|
+
return timer;
|
|
179
259
|
}
|
|
180
260
|
//# sourceMappingURL=frontend-sessions.js.map
|
package/dist/http/host-tools.js
CHANGED
|
@@ -71,6 +71,12 @@ export function applicableForPlatform({ entry, platform }, _optional = {}) {
|
|
|
71
71
|
export function defaultProbeEnv(_required, _optional = {}) {
|
|
72
72
|
return { access: ({ path, mode }) => fsAccess(path, mode), env: process.env, platform: currentPlatform({}) };
|
|
73
73
|
}
|
|
74
|
+
/** The login-shell bin directories a GUI-launched daemon's `PATH` tends to lack, per platform. */
|
|
75
|
+
function extraProbeDirs(platform, home) {
|
|
76
|
+
if (platform === 'darwin')
|
|
77
|
+
return ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`];
|
|
78
|
+
return platform === 'linux' ? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`] : [];
|
|
79
|
+
}
|
|
74
80
|
/**
|
|
75
81
|
* `$PATH` directories to probe, plus the common macOS/Linux locations a login shell would have
|
|
76
82
|
* that a GUI-launched daemon's thin inherited `PATH` often lacks (no `/usr/local/bin`, no
|
|
@@ -81,11 +87,7 @@ export function pathDirs(probeEnv) {
|
|
|
81
87
|
const raw = probeEnv.env.PATH ?? '';
|
|
82
88
|
const sep = probeEnv.platform === 'win32' ? ';' : ':';
|
|
83
89
|
const home = probeEnv.env.HOME ?? '';
|
|
84
|
-
const extras = probeEnv.platform
|
|
85
|
-
? ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`]
|
|
86
|
-
: probeEnv.platform === 'linux'
|
|
87
|
-
? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`]
|
|
88
|
-
: [];
|
|
90
|
+
const extras = extraProbeDirs(probeEnv.platform, home);
|
|
89
91
|
return [...raw.split(sep), ...extras].filter((dir) => dir.length > 0);
|
|
90
92
|
}
|
|
91
93
|
export async function probeCommandOnPath({ command, probeEnv }, _optional = {}) {
|
package/dist/http/index.d.ts
CHANGED
|
@@ -57,4 +57,5 @@ export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './
|
|
|
57
57
|
export type { RemoteAttachmentLookupOptional, RemoteAttachmentLookupRequired } from '../remote-attachment-lookup.js';
|
|
58
58
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
59
59
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
60
|
+
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
60
61
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -30,4 +30,5 @@ export { DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR, regi
|
|
|
30
30
|
export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './attachment-lookup-routes.js';
|
|
31
31
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
32
32
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
33
|
+
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
33
34
|
//# sourceMappingURL=index.js.map
|
package/dist/http/memory.js
CHANGED
|
@@ -290,7 +290,9 @@ export function registerMemoryEventStream({ app, deps, adapter }, _optional = {}
|
|
|
290
290
|
let seq = 0;
|
|
291
291
|
const channel = createSseChannel({ res });
|
|
292
292
|
const emit = (kind, data) => {
|
|
293
|
-
|
|
293
|
+
const cursor = seq;
|
|
294
|
+
seq += 1;
|
|
295
|
+
channel.enqueue({ event: { opaqueCursor: String(cursor), kind, data } });
|
|
294
296
|
};
|
|
295
297
|
const onChange = (event) => emit('change', event);
|
|
296
298
|
const onExtraction = (event) => emit('extraction', event);
|
package/dist/http/model-proxy.js
CHANGED
|
@@ -218,12 +218,12 @@ function toStreamEvent(seq, event) {
|
|
|
218
218
|
}
|
|
219
219
|
/** Answers a failure that happened before any SSE stream existed: a redacted JSON 500 if nothing was sent yet, otherwise just end the response (matching `mountJsonRoute`'s own SEC-005 catch). */
|
|
220
220
|
function respondInternalError(res, correlationId) {
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
res.end();
|
|
221
|
+
if (res.headersSent) {
|
|
222
|
+
if (!res.writableEnded)
|
|
223
|
+
res.end();
|
|
224
|
+
return;
|
|
226
225
|
}
|
|
226
|
+
sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
229
|
* The actual request handler body, shared by every fixed-path route and the `:provider`
|
|
@@ -238,6 +238,11 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
238
238
|
// reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
|
|
239
239
|
// before either exists yet.
|
|
240
240
|
let seq = 0;
|
|
241
|
+
const nextSeq = () => {
|
|
242
|
+
const current = seq;
|
|
243
|
+
seq += 1;
|
|
244
|
+
return current;
|
|
245
|
+
};
|
|
241
246
|
let channel;
|
|
242
247
|
try {
|
|
243
248
|
const origin = guardSameOrigin({ req, origin: adapter });
|
|
@@ -253,7 +258,7 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
253
258
|
const openChannel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
|
|
254
259
|
channel = openChannel;
|
|
255
260
|
openChannel.open();
|
|
256
|
-
await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(
|
|
261
|
+
await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(nextSeq(), event) }));
|
|
257
262
|
// No `channel.end()` call here: every reachable exit path inside every turn-runner emits
|
|
258
263
|
// exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
|
|
259
264
|
// see each provider module's own `turn-end-guard.ts` usage, and each function's own
|
|
@@ -283,8 +288,8 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
283
288
|
// here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
|
|
284
289
|
// no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
|
|
285
290
|
// event's own `isEndEvent` match closes the channel via the same path as a real one.
|
|
286
|
-
channel.enqueue({ event: toStreamEvent(
|
|
287
|
-
channel.enqueue({ event: toStreamEvent(
|
|
291
|
+
channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'error', message: 'an internal error occurred', code: correlationId }) });
|
|
292
|
+
channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'end', reason: 'error' }) });
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
295
|
/** Erases one provider's concrete parse/run types into the registry's shared entry shape — the single cast every entry needs. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Tavily default adapter. Request limits, timeout and secret-redaction policy remain here. */
|
|
2
2
|
import { validateBaseUrl } from '@jini-ai/agent-runtime';
|
|
3
3
|
import { redactSecrets } from '@jini-ai/core';
|
|
4
|
+
import { trimCharsEnd } from '@jini-ai/core/text';
|
|
4
5
|
const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
|
|
5
6
|
/** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
|
|
6
7
|
const TAVILY_MAX_RESULTS_LIMIT = 20;
|
|
@@ -20,7 +21,7 @@ function resolveTavilyBase(credentials) {
|
|
|
20
21
|
if (baseUrlCheck.error) {
|
|
21
22
|
throw new Error(baseUrlCheck.error);
|
|
22
23
|
}
|
|
23
|
-
return
|
|
24
|
+
return trimCharsEnd({ text: baseUrl, chars: '/' });
|
|
24
25
|
}
|
|
25
26
|
function trimmedOrUndefined(value) {
|
|
26
27
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
package/dist/http/routines.js
CHANGED
|
@@ -3,6 +3,8 @@ import { createApiError } from '@jini-ai/protocol';
|
|
|
3
3
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
4
4
|
import { validationError } from '@jini-ai/http-kit';
|
|
5
5
|
import { err, ok } from '@jini-ai/http-kit';
|
|
6
|
+
/** The 404 message for an unknown routine id. */
|
|
7
|
+
const ROUTINE_NOT_FOUND_MESSAGE = 'routine not found';
|
|
6
8
|
function isRecord(value) {
|
|
7
9
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
10
|
}
|
|
@@ -192,7 +194,7 @@ export const routineGetRoute = defineJsonRoute({
|
|
|
192
194
|
method: 'get', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
193
195
|
const routine = await deps.store.get({ id });
|
|
194
196
|
return routine === null
|
|
195
|
-
? err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
197
|
+
? err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) })
|
|
196
198
|
: ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
|
|
197
199
|
}
|
|
198
200
|
});
|
|
@@ -205,7 +207,7 @@ export const routineUpdateRoute = defineJsonRoute({
|
|
|
205
207
|
}
|
|
206
208
|
const routine = await deps.store.update({ id, patch });
|
|
207
209
|
if (routine === null)
|
|
208
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
210
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
209
211
|
deps.scheduler.rescheduleOne({ routineId: id });
|
|
210
212
|
return ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
|
|
211
213
|
}
|
|
@@ -214,14 +216,14 @@ export const routineDeleteRoute = defineJsonRoute({
|
|
|
214
216
|
method: 'delete', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
215
217
|
deps.scheduler.unschedule({ routineId: id });
|
|
216
218
|
const removed = await deps.store.delete({ id });
|
|
217
|
-
return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
219
|
+
return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
218
220
|
}
|
|
219
221
|
}, { requireSameOrigin: true });
|
|
220
222
|
export const routineRunNowRoute = defineJsonRoute({
|
|
221
223
|
method: 'post', path: '/api/routines/:id/run', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
222
224
|
const existing = await deps.store.get({ id });
|
|
223
225
|
if (existing === null)
|
|
224
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
226
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
225
227
|
const started = await deps.scheduler.runNow({ routineId: id });
|
|
226
228
|
const [routine, run] = await Promise.all([deps.store.get({ id }), deps.store.getLatestRun({ routineId: id })]);
|
|
227
229
|
return ok({
|
|
@@ -239,7 +241,7 @@ export const routineRunsListRoute = defineJsonRoute({
|
|
|
239
241
|
method: 'get', path: '/api/routines/:id/runs', parse: parseRoutineRunsQuery, handle: async ({ input: { id, limit }, deps }) => {
|
|
240
242
|
const existing = await deps.store.get({ id });
|
|
241
243
|
if (existing === null)
|
|
242
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
244
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
243
245
|
const runs = await deps.store.listRuns({ routineId: id, limit });
|
|
244
246
|
return ok({ value: { runs } });
|
|
245
247
|
}
|
package/dist/http/terminals.js
CHANGED
|
@@ -60,6 +60,11 @@ function toolResultToApiResult(deps, result) {
|
|
|
60
60
|
sink({ source: 'terminal-create', correlationId, error: result.status === 'failed' ? result.error : result.status });
|
|
61
61
|
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
62
62
|
}
|
|
63
|
+
default: {
|
|
64
|
+
// Unreachable: every tool result status has a case above, and a new one fails typecheck here.
|
|
65
|
+
const unhandled = result.status;
|
|
66
|
+
throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
function parseTerminalCreate(input) {
|
|
@@ -12,7 +12,7 @@ function parseSearchInput(input) {
|
|
|
12
12
|
const rawLimit = input.query.limit;
|
|
13
13
|
if (rawLimit === undefined)
|
|
14
14
|
return ok({ value: { query: rawQuery, limit: DEFAULT_SEARCH_LIMIT } });
|
|
15
|
-
if (typeof rawLimit !== 'string' ||
|
|
15
|
+
if (typeof rawLimit !== 'string' || !/^\d+$/.test(rawLimit)) {
|
|
16
16
|
return err({ error: validationError({ message: 'limit must be a single positive-integer query-string value when provided' }) });
|
|
17
17
|
}
|
|
18
18
|
const parsed = Number(rawLimit);
|
package/dist/http/xai.js
CHANGED
|
@@ -123,6 +123,9 @@ import { createApiError } from '@jini-ai/protocol';
|
|
|
123
123
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
124
124
|
import { validationError } from '@jini-ai/http-kit';
|
|
125
125
|
import { err, ok } from '@jini-ai/http-kit';
|
|
126
|
+
import { trimCharsEnd } from '@jini-ai/core/text';
|
|
127
|
+
/** The redacted message every internal failure answers with; details go to the error sink. */
|
|
128
|
+
const INTERNAL_ERROR_MESSAGE = 'an internal error occurred';
|
|
126
129
|
const DEFAULT_TOKEN_FILE_NAME = 'xai-oauth-token.json';
|
|
127
130
|
/** Matches the loopback listener's own 30-minute self-close timeout (OD's real comment: keeps the
|
|
128
131
|
* PKCE state, the open socket, and any paste-back UI expiring together). */
|
|
@@ -361,7 +364,7 @@ export const xaiOauthStartRoute = defineJsonRoute({
|
|
|
361
364
|
await stopListener(resolved);
|
|
362
365
|
const correlationId = randomUUID();
|
|
363
366
|
resolved.onInternalError({ source: 'oauth-start', correlationId, error });
|
|
364
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
367
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
365
368
|
}
|
|
366
369
|
});
|
|
367
370
|
}
|
|
@@ -410,7 +413,7 @@ export const xaiOauthCompleteRoute = defineJsonRoute({
|
|
|
410
413
|
}
|
|
411
414
|
const correlationId = randomUUID();
|
|
412
415
|
resolved.onInternalError({ source: 'oauth-complete', correlationId, error: redactError(error) });
|
|
413
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
416
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
}, { requireSameOrigin: true });
|
|
@@ -448,7 +451,7 @@ export const xaiAuthStatusRoute = defineJsonRoute({
|
|
|
448
451
|
catch (error) {
|
|
449
452
|
const correlationId = randomUUID();
|
|
450
453
|
resolved.onInternalError({ source: 'auth-status', correlationId, error });
|
|
451
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
454
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
452
455
|
}
|
|
453
456
|
}
|
|
454
457
|
}, { requireSameOrigin: true });
|
|
@@ -466,7 +469,7 @@ export const xaiOauthDisconnectRoute = defineJsonRoute({
|
|
|
466
469
|
catch (error) {
|
|
467
470
|
const correlationId = randomUUID();
|
|
468
471
|
resolved.onInternalError({ source: 'oauth-disconnect', correlationId, error });
|
|
469
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
472
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
470
473
|
}
|
|
471
474
|
}
|
|
472
475
|
}, { requireSameOrigin: true });
|
|
@@ -594,7 +597,7 @@ async function callXaiSearch(resolved, accessToken, request) {
|
|
|
594
597
|
const baseUrlCheck = validateBaseUrl({ baseUrl: resolved.searchBaseUrl });
|
|
595
598
|
if (baseUrlCheck.error)
|
|
596
599
|
throw new Error(baseUrlCheck.error);
|
|
597
|
-
const base = resolved.searchBaseUrl
|
|
600
|
+
const base = trimCharsEnd({ text: resolved.searchBaseUrl, chars: '/' });
|
|
598
601
|
const model = request.model ?? resolved.searchDefaultModel;
|
|
599
602
|
const requestBody = {
|
|
600
603
|
model,
|
|
@@ -633,7 +636,7 @@ export const xaiSearchRoute = defineJsonRoute({
|
|
|
633
636
|
catch (error) {
|
|
634
637
|
const correlationId = randomUUID();
|
|
635
638
|
resolved.onInternalError({ source: 'search', correlationId, error });
|
|
636
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
639
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
637
640
|
}
|
|
638
641
|
if (!credential) {
|
|
639
642
|
// No entitlement gate here — see module doc's "Dropped: OD's SuperGrok subscription gate"
|
|
@@ -649,7 +652,7 @@ export const xaiSearchRoute = defineJsonRoute({
|
|
|
649
652
|
catch (error) {
|
|
650
653
|
const correlationId = randomUUID();
|
|
651
654
|
resolved.onInternalError({ source: 'search', correlationId, error });
|
|
652
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
655
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
653
656
|
}
|
|
654
657
|
}
|
|
655
658
|
}, { requireSameOrigin: true });
|
package/dist/index.d.ts
CHANGED
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
* solution belongs at the lifecycle or transport boundary, rather than a
|
|
73
73
|
* restart-unsafe recorder cache that makes the two paths disagree.
|
|
74
74
|
*
|
|
75
|
-
* node-pty
|
|
76
|
-
* C/C++ compiler
|
|
75
|
+
* The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
|
|
76
|
+
* Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
|
|
77
77
|
* Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
|
|
78
78
|
* See the owning modules and packages/daemon/README.md for detailed contracts.
|
|
79
79
|
*/
|
package/dist/index.js
CHANGED
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
* solution belongs at the lifecycle or transport boundary, rather than a
|
|
73
73
|
* restart-unsafe recorder cache that makes the two paths disagree.
|
|
74
74
|
*
|
|
75
|
-
* node-pty
|
|
76
|
-
* C/C++ compiler
|
|
75
|
+
* The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
|
|
76
|
+
* Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
|
|
77
77
|
* Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
|
|
78
78
|
* See the owning modules and packages/daemon/README.md for detailed contracts.
|
|
79
79
|
*/
|
|
@@ -122,18 +122,6 @@ export declare function dataDirHasExistingPayload({ dataDir, config }: {
|
|
|
122
122
|
}, { filesystem: fs }?: {
|
|
123
123
|
filesystem?: LegacyFilesystemPort;
|
|
124
124
|
}): string[];
|
|
125
|
-
/**
|
|
126
|
-
* Move staged payload entries into the final `dataDir`. Each entry is moved
|
|
127
|
-
* with `renameSync` (atomic on the same filesystem); a cross-device rename
|
|
128
|
-
* falls back to copy + remove. Rollback-safe in two layers: the copy
|
|
129
|
-
* fallback removes a partial destination on its own failure, and the outer
|
|
130
|
-
* loop removes every entry it already promoted before rethrowing.
|
|
131
|
-
*
|
|
132
|
-
* @param stagingDir - The sibling staging directory holding the copied payload.
|
|
133
|
-
* @param dataDir - The destination data root.
|
|
134
|
-
* @param entries - The payload entries to promote.
|
|
135
|
-
* @returns The entries that were placed into `dataDir` before any failure.
|
|
136
|
-
*/
|
|
137
125
|
export declare function promoteStaged({ stagingDir, dataDir, entries }: {
|
|
138
126
|
readonly stagingDir: string;
|
|
139
127
|
readonly dataDir: string;
|
|
@@ -133,6 +133,26 @@ function stagePayload(legacyDir, stagingDir, payloadEntries, fs) {
|
|
|
133
133
|
* @param entries - The payload entries to promote.
|
|
134
134
|
* @returns The entries that were placed into `dataDir` before any failure.
|
|
135
135
|
*/
|
|
136
|
+
/** Renames `src` to `dst`, falling back to copy-then-delete (e.g. across devices); a failed copy
|
|
137
|
+
* removes its partial `dst` before rethrowing. */
|
|
138
|
+
function moveEntry(src, dst, fs) {
|
|
139
|
+
try {
|
|
140
|
+
fs.renameSync(src, dst);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
copyThenRemove(src, dst, fs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function copyThenRemove(src, dst, fs) {
|
|
147
|
+
try {
|
|
148
|
+
fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
149
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
catch (cpErr) {
|
|
152
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
153
|
+
throw cpErr;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
136
156
|
export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
137
157
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
138
158
|
const promoted = [];
|
|
@@ -140,19 +160,7 @@ export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs
|
|
|
140
160
|
for (const entry of entries) {
|
|
141
161
|
const src = path.join(stagingDir, entry);
|
|
142
162
|
const dst = path.join(dataDir, entry);
|
|
143
|
-
|
|
144
|
-
fs.renameSync(src, dst);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
try {
|
|
148
|
-
fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
149
|
-
fs.rmSync(src, { recursive: true, force: true });
|
|
150
|
-
}
|
|
151
|
-
catch (cpErr) {
|
|
152
|
-
fs.rmSync(dst, { recursive: true, force: true });
|
|
153
|
-
throw cpErr;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
163
|
+
moveEntry(src, dst, fs);
|
|
156
164
|
promoted.push(entry);
|
|
157
165
|
}
|
|
158
166
|
}
|
|
@@ -146,7 +146,8 @@ function readToolStream({ res, settle, fail }, { emitSurface }) {
|
|
|
146
146
|
res.on('data', (chunk) => {
|
|
147
147
|
buffer += chunk;
|
|
148
148
|
const lines = buffer.split('\n');
|
|
149
|
-
|
|
149
|
+
// String.split always produces at least one member, including for an empty chunk.
|
|
150
|
+
buffer = lines.pop();
|
|
150
151
|
lines.forEach(onLine);
|
|
151
152
|
});
|
|
152
153
|
// Once the terminal line is in hand the outcome is known, so a later reset cannot unsettle it.
|
|
@@ -110,7 +110,8 @@ export function tzWallToUtcGapFallback({ timezone, year, month, day, hour, minut
|
|
|
110
110
|
}
|
|
111
111
|
function matchesWallClock(timezone, at, year, month, day, hour, minute) {
|
|
112
112
|
const p = partsInTimezone({ timezone: timezone, atUtc: at });
|
|
113
|
-
|
|
113
|
+
const sameDate = p.year === year && p.month === month && p.day === day;
|
|
114
|
+
return sameDate && p.hour === hour && p.minute === minute;
|
|
114
115
|
}
|
|
115
116
|
/** Minutes east of UTC for `timezone` at instant `at`. e.g. Asia/Shanghai returns 480. */
|
|
116
117
|
function tzOffsetMinutes(timezone, at) {
|
|
@@ -43,6 +43,11 @@ export class ScheduledRunPersistenceError extends Error {
|
|
|
43
43
|
this.name = 'ScheduledRunPersistenceError';
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/** What a failed scheduled run logs: the underlying error's message when there is one. */
|
|
47
|
+
function scheduledRunFailureDetail(error) {
|
|
48
|
+
const cause = error instanceof ScheduledRunPersistenceError ? error.originalError : error;
|
|
49
|
+
return cause instanceof Error ? cause.message : cause;
|
|
50
|
+
}
|
|
46
51
|
function isScheduledRunPersistenceError(error) {
|
|
47
52
|
return error instanceof ScheduledRunPersistenceError;
|
|
48
53
|
}
|
|
@@ -136,13 +141,7 @@ export class RoutineService {
|
|
|
136
141
|
this.rescheduleOne({ routineId: routine.id });
|
|
137
142
|
})
|
|
138
143
|
.catch((error) => {
|
|
139
|
-
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, error
|
|
140
|
-
? error.originalError instanceof Error
|
|
141
|
-
? error.originalError.message
|
|
142
|
-
: error.originalError
|
|
143
|
-
: error instanceof Error
|
|
144
|
-
? error.message
|
|
145
|
-
: error);
|
|
144
|
+
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, scheduledRunFailureDetail(error));
|
|
146
145
|
if (isScheduledRunPersistenceError(error)) {
|
|
147
146
|
this.retryScheduledSlot(routine.id, fireAt);
|
|
148
147
|
}
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -101,7 +101,8 @@ function terminalStateFromEndEntry(entry) {
|
|
|
101
101
|
if (!isRecord(entry.data))
|
|
102
102
|
return { state: 'failed', resumable: false };
|
|
103
103
|
const status = entry.data.status;
|
|
104
|
-
const
|
|
104
|
+
const unsuccessful = status === 'canceled' ? 'cancelled' : 'failed';
|
|
105
|
+
const state = status === 'succeeded' ? 'succeeded' : unsuccessful;
|
|
105
106
|
return { state, resumable: entry.data.resumable === true };
|
|
106
107
|
}
|
|
107
108
|
// ---------------------------------------------------------------------------
|