@stratusagent/gateway 0.13.7 → 0.14.0
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/dist/event-intake.d.ts +141 -0
- package/dist/event-intake.d.ts.map +1 -0
- package/dist/event-intake.js +369 -0
- package/dist/event-intake.js.map +1 -0
- package/dist/events.d.ts +256 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +467 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +155 -16
- package/dist/index.js.map +1 -1
- package/dist/schedules.d.ts +41 -0
- package/dist/schedules.d.ts.map +1 -1
- package/dist/schedules.js +306 -39
- package/dist/schedules.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -9,17 +9,21 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
10
10
|
import { mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { filePathsOf, isUnaddressedTurn, carriesKey, leastTrusted, senderTrustOf, SENDER_TRUST_METADATA_KEY, workItemState, lastResponseIsAnswer, MAX_IDEMPOTENCY_KEY_LENGTH, recordTurnFailure, completeAnsweredTurn, abortErrorFor, sessionTrustOf, sessionWriteTrust, AgentRegistry, AgentRunner, ChannelRegistry, ContributionRegistry, createRoutedMemoryStore, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, CONTEXT_FLOOR_METADATA_KEY, PENDING_APPROVAL_METADATA_KEY, SESSION_TAINTED_BY_METADATA_KEY, SESSION_TRUST_METADATA_KEY, latestTurnReply, readPendingApproval, conversationContextFrom, CONVERSATION_METADATA_KEY, STRATUS_SKILL_ID, imagesWithinReplayBudget, matchesToolAllowlist, } from '@stratusagent/core';
|
|
13
|
-
import { CREDENTIAL_REQUEST_TOOL_NAME, createCredentialRequestTool, createDelegateTool, DELEGATE_TOOL_NAME, createForgetTool, createPinTool, createMessageSendTool, createMessageReadTool, createChannelCreateTool, directMessageReadRefusal, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DEFAULT_MAX_DELEGATION_DEPTH, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, loadStratusSkill, STRATUS_SKILL_PATH, } from '@stratusagent/agents';
|
|
12
|
+
import { filePathsOf, isUnaddressedTurn, carriesKey, leastTrusted, senderTrustOf, SENDER_TRUST_METADATA_KEY, workItemState, leftForNextStart, lastResponseIsAnswer, MAX_IDEMPOTENCY_KEY_LENGTH, recordTurnFailure, completeAnsweredTurn, abortErrorFor, sessionTrustOf, sessionWriteTrust, AgentRegistry, AgentRunner, ChannelRegistry, ContributionRegistry, createRoutedMemoryStore, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, CONTEXT_FLOOR_METADATA_KEY, PENDING_APPROVAL_METADATA_KEY, SESSION_TAINTED_BY_METADATA_KEY, SESSION_TRUST_METADATA_KEY, latestTurnReply, readPendingApproval, conversationContextFrom, CONVERSATION_METADATA_KEY, STRATUS_SKILL_ID, imagesWithinReplayBudget, matchesToolAllowlist, } from '@stratusagent/core';
|
|
13
|
+
import { CREDENTIAL_REQUEST_TOOL_NAME, createCredentialRequestTool, createDelegateTool, DELEGATE_TOOL_NAME, createForgetTool, createPinTool, createMessageSendTool, createMessageReadTool, createChannelCreateTool, directMessageReadRefusal, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DEFAULT_MAX_DELEGATION_DEPTH, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, loadStratusSkill, STRATUS_SKILL_PATH, credentialVariableProblem, } from '@stratusagent/agents';
|
|
14
14
|
import { createFleetReader } from "./fleet.js";
|
|
15
|
+
import { createEventIntake, summarizeEvents, } from "./event-intake.js";
|
|
16
|
+
import { SqliteEventStore } from "./events.js";
|
|
15
17
|
import { createSchedulerRuntime, isScheduleSessionId, SqliteScheduleStore, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
16
18
|
import { ShardedSessionStore } from "./sessions.js";
|
|
17
19
|
export { claimHome, homeLockPath, HomeClaimedError, } from "./lock.js";
|
|
18
20
|
export { SqliteSessionStore, ShardedSessionStore, FleetSessionIndex, SessionIdTakenError, tightenSqliteFile, } from "./sessions.js";
|
|
19
21
|
export { SqliteScheduleStore, createSchedulerRuntime, isScheduleSessionId, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
22
|
+
export { SqliteEventStore, EVENT_BLOCK_MAX_BYTES, EVENT_DEDUP_WINDOW_MS, EVENT_DELIVERY_ROW_CAP, eventCoalescingKey, eventDispatchKey, eventSessionId, renderEventBlock, } from "./events.js";
|
|
23
|
+
export { createEventIntake, mapGithubDelivery, parseStandardWebhookEvent, verifyGithubSignature, verifyStandardWebhookSignature, DEFAULT_EVENT_MAX_BODY_BYTES, EVENT_SOURCE_REQUESTS_PER_MINUTE, } from "./event-intake.js";
|
|
20
24
|
import { createLocalCommandExecutor } from '@stratusagent/executor-local';
|
|
21
25
|
import { loadPlugins, } from '@stratusagent/plugins';
|
|
22
|
-
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver, createHomeMemoryStore, drainSharedMemory, hasBracketedLegacyStateIn, createRuntimeProvider, describeServingModel, DEFAULT_STRATUS_AGENT, isRegisteredProviderName, loadChannelTransportSecrets, registeredProviderNameOf, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, grantSoulCredential, withSoulFileLock, addNamedCredential, appendCredentialAudit, replaceNamedCredential, NamedCredentialMissingError, namedCredentialSource, NamedCredentialExistsError, CREDENTIAL_NAME_PATTERN, quoteShellArg, migrateLegacyMemory, PROVIDER_STATE_METADATA_KEYS, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, runsOnHarness, applySoulPins, assertStateCompatible, stratusHomePath, intakeStatePath, fleetDbIn, createAgentWorkspaces, createPluginStateDirectories, createHostProtectedPaths, foldedAgentId, syncDirectory, writeFileSynced, } from '@stratusagent/state';
|
|
26
|
+
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver, resolveDaemonCredential, createHomeMemoryStore, drainSharedMemory, hasBracketedLegacyStateIn, createRuntimeProvider, describeServingModel, DEFAULT_STRATUS_AGENT, isRegisteredProviderName, loadChannelTransportSecrets, registeredProviderNameOf, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, grantSoulCredential, withSoulFileLock, addNamedCredential, appendCredentialAudit, replaceNamedCredential, NamedCredentialMissingError, namedCredentialSource, NamedCredentialExistsError, CREDENTIAL_NAME_PATTERN, quoteShellArg, migrateLegacyMemory, PROVIDER_STATE_METADATA_KEYS, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, runsOnHarness, applySoulPins, assertStateCompatible, stratusHomePath, intakeStatePath, fleetDbIn, createAgentWorkspaces, createPluginStateDirectories, createHostProtectedPaths, foldedAgentId, syncDirectory, writeFileSynced, } from '@stratusagent/state';
|
|
23
27
|
const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
|
|
24
28
|
/**
|
|
25
29
|
* How long a parked call waits for a person. Long enough to survive a
|
|
@@ -393,6 +397,27 @@ export const createGateway = (options = {}) => {
|
|
|
393
397
|
// The same database sessions live in — one file to back up — through the
|
|
394
398
|
// schedule store's own connection (see its docs for why).
|
|
395
399
|
const scheduleStore = new SqliteScheduleStore(fleetDbIn(stateDir), { stateHome: stateDir });
|
|
400
|
+
// ---- events (card 590) ---------------------------------------------------
|
|
401
|
+
// Opened only for a daemon with sources, so one that never takes events
|
|
402
|
+
// never grows the tables. Its own connection to fleet.db, like schedules'.
|
|
403
|
+
const eventSources = options.events?.sources ?? {};
|
|
404
|
+
const eventStore = Object.keys(eventSources).length > 0
|
|
405
|
+
? new SqliteEventStore(fleetDbIn(stateDir), { stateHome: stateDir })
|
|
406
|
+
: undefined;
|
|
407
|
+
const eventIntake = eventStore
|
|
408
|
+
? createEventIntake({
|
|
409
|
+
store: eventStore,
|
|
410
|
+
sources: () => eventSources,
|
|
411
|
+
// The daemon's own credential, read per delivery: a stored or
|
|
412
|
+
// rotated secret works on the next one, with no restart.
|
|
413
|
+
secret: (name) => resolveDaemonCredential(env, name),
|
|
414
|
+
// The triggers are schedules: `schedule.on` rows on this source.
|
|
415
|
+
// Late-bound, because the scheduler is built further down.
|
|
416
|
+
triggers: (sourceId) => scheduler.eventTriggers(sourceId),
|
|
417
|
+
...(options.events?.maxBodyBytes !== undefined ? { maxBodyBytes: options.events.maxBodyBytes } : {}),
|
|
418
|
+
warn,
|
|
419
|
+
})
|
|
420
|
+
: undefined;
|
|
396
421
|
/**
|
|
397
422
|
* The started adapter of one kind that speaks for an agent and has the
|
|
398
423
|
* capability asked for. `carriesOthers` says there were capable adapters
|
|
@@ -462,6 +487,7 @@ export const createGateway = (options = {}) => {
|
|
|
462
487
|
// `SchedulerRuntimeOptions.ready`.
|
|
463
488
|
ready: () => assertStateCompatible(env),
|
|
464
489
|
paused: () => intakeState.paused,
|
|
490
|
+
...(eventStore ? { events: { store: eventStore, sources: () => Object.keys(eventSources) } } : {}),
|
|
465
491
|
log,
|
|
466
492
|
warn,
|
|
467
493
|
});
|
|
@@ -1367,11 +1393,26 @@ export const createGateway = (options = {}) => {
|
|
|
1367
1393
|
throw new Error(`${JSON.stringify(request.name)} is not a credential name. Use letters, digits, dots, dashes, or underscores, `
|
|
1368
1394
|
+ 'starting with a letter, the way the tool that needs it spells it: search.apiKey, github.token.');
|
|
1369
1395
|
}
|
|
1396
|
+
// The stricter rule, for a name the model picked: checked before anyone
|
|
1397
|
+
// is asked, so an approver never sees a form whose binding the store
|
|
1398
|
+
// would refuse, and so a variable that changes what a judged command
|
|
1399
|
+
// does (NODE_OPTIONS) is never offered at all.
|
|
1400
|
+
if (request.env !== undefined) {
|
|
1401
|
+
const problem = credentialVariableProblem(request.env, { requested: true });
|
|
1402
|
+
if (problem !== undefined) {
|
|
1403
|
+
throw new Error(`${problem} Nothing is pending.`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1370
1406
|
// A key already stored is not something to ask a person for again: the
|
|
1371
1407
|
// form only adds, so it could only fail. Say what is actually missing.
|
|
1372
1408
|
const source = await namedCredentialSource(env, agentId, request.name);
|
|
1373
1409
|
const stored = source !== undefined;
|
|
1374
1410
|
const granted = session.agent.credentials?.includes(request.name) === true;
|
|
1411
|
+
// Binding a key the agent already holds asks for no value, and every
|
|
1412
|
+
// form today has a value field, so phase 1 leaves it to the machine.
|
|
1413
|
+
const bindOnMachine = request.env !== undefined
|
|
1414
|
+
? ` To have your shell read it as ${request.env}, ask your operator to run \`stratus credential env ${request.name} ${request.env} --agent ${quoteShellArg(agentId)}\` on the machine.`
|
|
1415
|
+
: '';
|
|
1375
1416
|
if (request.replace === true) {
|
|
1376
1417
|
// Checked here, before anyone is asked, so an approver is never shown
|
|
1377
1418
|
// a form whose answer could only be refused.
|
|
@@ -1394,13 +1435,18 @@ export const createGateway = (options = {}) => {
|
|
|
1394
1435
|
}
|
|
1395
1436
|
}
|
|
1396
1437
|
else if (stored && granted) {
|
|
1397
|
-
throw new Error(`You already hold ${request.name}; the tools that need it use it for you. To rotate it for a new value, ask again with replace: true
|
|
1438
|
+
throw new Error(`You already hold ${request.name}; the tools that need it use it for you. To rotate it for a new value, ask again with replace: true.${bindOnMachine}`
|
|
1439
|
+
+ (request.env !== undefined ? ' Nothing is pending.' : ''));
|
|
1398
1440
|
}
|
|
1399
1441
|
else if (stored) {
|
|
1400
1442
|
throw new Error(`${request.name} is already ${source === 'environment' ? "supplied by the daemon's environment" : 'stored'} but not granted to you. `
|
|
1401
|
-
+
|
|
1443
|
+
+ `Ask your operator to add it to the credentials list in your soul; a form would only refuse to store it again.${bindOnMachine}`);
|
|
1402
1444
|
}
|
|
1403
|
-
const onMachine =
|
|
1445
|
+
const onMachine = request.env === undefined
|
|
1446
|
+
? `\`stratus credential set ${request.name}${request.scope === 'agent' ? ` --agent ${quoteShellArg(agentId)}` : ''}\``
|
|
1447
|
+
: request.scope === 'agent'
|
|
1448
|
+
? `\`stratus credential set ${request.name} --agent ${quoteShellArg(agentId)} --env ${request.env}\``
|
|
1449
|
+
: `\`stratus credential set ${request.name}\`, then \`stratus credential env ${request.name} ${request.env} --agent ${quoteShellArg(agentId)}\``;
|
|
1404
1450
|
const record = {
|
|
1405
1451
|
agentId,
|
|
1406
1452
|
sessionId: session.id,
|
|
@@ -1408,6 +1454,7 @@ export const createGateway = (options = {}) => {
|
|
|
1408
1454
|
scope: request.scope,
|
|
1409
1455
|
soulPath,
|
|
1410
1456
|
...(request.replace === true ? { replace: true } : {}),
|
|
1457
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1411
1458
|
};
|
|
1412
1459
|
const announce = async (requestId, via) => {
|
|
1413
1460
|
await bus.emit({
|
|
@@ -1420,6 +1467,7 @@ export const createGateway = (options = {}) => {
|
|
|
1420
1467
|
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
1421
1468
|
via,
|
|
1422
1469
|
...(request.replace === true ? { replace: true } : {}),
|
|
1470
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1423
1471
|
...(session.metadata ? { metadata: session.metadata } : {}),
|
|
1424
1472
|
});
|
|
1425
1473
|
};
|
|
@@ -1440,11 +1488,14 @@ export const createGateway = (options = {}) => {
|
|
|
1440
1488
|
const kind = session.metadata?.channel;
|
|
1441
1489
|
const { adapter } = typeof kind === 'string'
|
|
1442
1490
|
? channelCarrying(kind, agentId, (candidate) => candidate.requestCredential !== undefined
|
|
1443
|
-
&& (request.replace !== true || candidate.credentialReplaceForms === true)
|
|
1491
|
+
&& (request.replace !== true || candidate.credentialReplaceForms === true)
|
|
1492
|
+
// A form that can't say the key will be shell-readable must not
|
|
1493
|
+
// be the approval for it: a link says it instead.
|
|
1494
|
+
&& (request.env === undefined || candidate.credentialEnvForms === true))
|
|
1444
1495
|
: { adapter: undefined };
|
|
1445
1496
|
if (!adapter?.requestCredential || !session.metadata) {
|
|
1446
1497
|
formUnavailable = typeof kind === 'string'
|
|
1447
|
-
? `This conversation's channel (${kind}) cannot show your operator a ${request.replace === true ? 'replacement' : 'credential'} form here.`
|
|
1498
|
+
? `This conversation's channel (${kind}) cannot show your operator a ${request.replace === true ? 'replacement' : 'credential'} form here${request.env !== undefined ? ' that says which variable your shell will read it from' : ''}.`
|
|
1448
1499
|
: 'This conversation is not in a channel that can show your operator a form.';
|
|
1449
1500
|
}
|
|
1450
1501
|
else {
|
|
@@ -1463,6 +1514,7 @@ export const createGateway = (options = {}) => {
|
|
|
1463
1514
|
scope: request.scope,
|
|
1464
1515
|
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
1465
1516
|
...(request.replace === true ? { replace: true } : {}),
|
|
1517
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1466
1518
|
metadata: session.metadata,
|
|
1467
1519
|
});
|
|
1468
1520
|
await announce(requestId, 'form');
|
|
@@ -1504,6 +1556,7 @@ export const createGateway = (options = {}) => {
|
|
|
1504
1556
|
name: request.name,
|
|
1505
1557
|
scope: request.scope,
|
|
1506
1558
|
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
1559
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1507
1560
|
});
|
|
1508
1561
|
}
|
|
1509
1562
|
catch (error) {
|
|
@@ -1558,6 +1611,7 @@ export const createGateway = (options = {}) => {
|
|
|
1558
1611
|
...(actor !== undefined ? { actor } : {}),
|
|
1559
1612
|
via: request.via ?? 'form',
|
|
1560
1613
|
requestedBy: request.agentId,
|
|
1614
|
+
...(request.env !== undefined ? { env: request.env, bindingFor: request.agentId } : {}),
|
|
1561
1615
|
});
|
|
1562
1616
|
}
|
|
1563
1617
|
catch (error) {
|
|
@@ -1600,7 +1654,12 @@ export const createGateway = (options = {}) => {
|
|
|
1600
1654
|
message: `${request.soulPath} now belongs to ${owner}, not ${request.agentId}, so nothing was stored. Ask ${request.agentId} to request it again.`,
|
|
1601
1655
|
};
|
|
1602
1656
|
}
|
|
1603
|
-
const store =
|
|
1657
|
+
const store = {
|
|
1658
|
+
...(request.scope === 'agent' ? { agentId: request.agentId } : {}),
|
|
1659
|
+
// The binding the form promised, written with the value, always for
|
|
1660
|
+
// the agent that asked: consent is per agent even for a shared key.
|
|
1661
|
+
...(request.env !== undefined ? { bind: { agentId: request.agentId, variable: request.env } } : {}),
|
|
1662
|
+
};
|
|
1604
1663
|
if (request.replace === true) {
|
|
1605
1664
|
// Re-read under the soul lock, so a grant removed while the form
|
|
1606
1665
|
// waited (a shared key most of all) is never rotated on its behalf.
|
|
@@ -1641,6 +1700,7 @@ export const createGateway = (options = {}) => {
|
|
|
1641
1700
|
name: request.name,
|
|
1642
1701
|
scope: request.scope,
|
|
1643
1702
|
replace: true,
|
|
1703
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1644
1704
|
...(input.actor !== undefined ? { actor: input.actor } : {}),
|
|
1645
1705
|
});
|
|
1646
1706
|
// The soul already lists it: a replace is only asked for a key held.
|
|
@@ -1699,6 +1759,7 @@ export const createGateway = (options = {}) => {
|
|
|
1699
1759
|
requestId: input.requestId,
|
|
1700
1760
|
name: request.name,
|
|
1701
1761
|
scope: request.scope,
|
|
1762
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1702
1763
|
...(input.actor !== undefined ? { actor: input.actor } : {}),
|
|
1703
1764
|
...(grantError !== undefined ? { grantError } : {}),
|
|
1704
1765
|
});
|
|
@@ -2092,7 +2153,7 @@ export const createGateway = (options = {}) => {
|
|
|
2092
2153
|
const idle = new AbortController();
|
|
2093
2154
|
trackTurn(sessionId, idle);
|
|
2094
2155
|
if (abortingWith !== undefined) {
|
|
2095
|
-
idle.abort(
|
|
2156
|
+
idle.abort(shutdownAbort());
|
|
2096
2157
|
}
|
|
2097
2158
|
try {
|
|
2098
2159
|
return await run(idle.signal);
|
|
@@ -2104,7 +2165,7 @@ export const createGateway = (options = {}) => {
|
|
|
2104
2165
|
const controller = new AbortController();
|
|
2105
2166
|
trackTurn(sessionId, controller);
|
|
2106
2167
|
if (abortingWith !== undefined) {
|
|
2107
|
-
controller.abort(
|
|
2168
|
+
controller.abort(shutdownAbort());
|
|
2108
2169
|
}
|
|
2109
2170
|
// The external reason travels: a caller that aborted with its own
|
|
2110
2171
|
// `RunAbortedError` gets it recorded on the session, like the watchdog's.
|
|
@@ -2348,6 +2409,22 @@ export const createGateway = (options = {}) => {
|
|
|
2348
2409
|
const refusal = () => restartStatus
|
|
2349
2410
|
? new GatewayStoppingError('stratusd is restarting and will accept new work once it is back up.', true)
|
|
2350
2411
|
: new GatewayStoppingError('The gateway is stopping and no longer accepts new work.', false);
|
|
2412
|
+
/**
|
|
2413
|
+
* What the caller of a keyed turn the shutdown cut short hears: the same
|
|
2414
|
+
* refusal, since its message is no more answered than one refused at the
|
|
2415
|
+
* door (a channel keeps it for the next start, as it keeps those), and
|
|
2416
|
+
* worded for a turn that ran and will be continued.
|
|
2417
|
+
*/
|
|
2418
|
+
const leftForNext = () => restartStatus
|
|
2419
|
+
? new GatewayStoppingError('stratusd is restarting; this turn was cut short and continues once it is back up.', true)
|
|
2420
|
+
: new GatewayStoppingError('stratusd is stopping; this turn was cut short and continues when it is started again.', false);
|
|
2421
|
+
/**
|
|
2422
|
+
* The abort a stop's or restart's window ends a turn with. Resumable: a
|
|
2423
|
+
* keyed turn is left for the next start to continue once, as a crash
|
|
2424
|
+
* leaves it, rather than failed (`RunAbortedError.resumable`); every
|
|
2425
|
+
* other turn is failed with the reason, as before.
|
|
2426
|
+
*/
|
|
2427
|
+
const shutdownAbort = () => new RunAbortedError(abortingWith, { resumable: true });
|
|
2351
2428
|
/**
|
|
2352
2429
|
* The turn id running on each session, for callers that named one.
|
|
2353
2430
|
*
|
|
@@ -2478,6 +2555,27 @@ export const createGateway = (options = {}) => {
|
|
|
2478
2555
|
? runsOnHarness(fallback)
|
|
2479
2556
|
: runsOnHarness(config) || (fallback !== undefined && runsOnHarness(fallback));
|
|
2480
2557
|
};
|
|
2558
|
+
/**
|
|
2559
|
+
* A keyed turn the shutdown cut short and the runner left for the next
|
|
2560
|
+
* start (`leftForNextStart`): its caller is told so, the way a message
|
|
2561
|
+
* refused at the door is, and keeps it for the next daemon, which
|
|
2562
|
+
* continues the turn. Read back on the chain, so it is this dispatch's
|
|
2563
|
+
* own message the session is still running on.
|
|
2564
|
+
*/
|
|
2565
|
+
const settleLeftForNextStart = async (input, turn) => {
|
|
2566
|
+
try {
|
|
2567
|
+
return await turn;
|
|
2568
|
+
}
|
|
2569
|
+
catch (error) {
|
|
2570
|
+
if (input.idempotencyKey !== undefined && error instanceof RunAbortedError && error.resumable) {
|
|
2571
|
+
const left = await store.get(input.sessionId).catch(() => undefined);
|
|
2572
|
+
if (left !== undefined && workItemState(left, input.idempotencyKey) === 'unfinished' && leftForNextStart(left, error)) {
|
|
2573
|
+
throw leftForNext();
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
throw error;
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2481
2579
|
/**
|
|
2482
2580
|
* `subAgent` is the narrowed definition `subAgents.run` built: the turn
|
|
2483
2581
|
* runs as that, on the parent's resolved provider, with no memory and no
|
|
@@ -2546,7 +2644,7 @@ export const createGateway = (options = {}) => {
|
|
|
2546
2644
|
// has switched for good, the primary is never called again, and only
|
|
2547
2645
|
// the fallback counts.
|
|
2548
2646
|
const hostedLoop = input.idempotencyKey !== undefined && reachesHarness(config, switchedToFallback);
|
|
2549
|
-
return withWatchdog(input.sessionId, input.signal, effectiveStreams, fallbackStreams, async (signal) => {
|
|
2647
|
+
return settleLeftForNextStart(input, withWatchdog(input.sessionId, input.signal, effectiveStreams, fallbackStreams, async (signal) => {
|
|
2550
2648
|
// The preflight above (agent refresh, config resolution, session
|
|
2551
2649
|
// load) awaited filesystem work; the entry check has long passed.
|
|
2552
2650
|
// Recheck before the runner touches durable state, or a dispatch
|
|
@@ -2625,7 +2723,7 @@ export const createGateway = (options = {}) => {
|
|
|
2625
2723
|
runtime: runtimeContextFor(source, config, switchedToFallback, input.metadata, subAgent !== undefined),
|
|
2626
2724
|
signal,
|
|
2627
2725
|
});
|
|
2628
|
-
});
|
|
2726
|
+
}));
|
|
2629
2727
|
};
|
|
2630
2728
|
/**
|
|
2631
2729
|
* Finishes the turns that were parked on a human when this daemon last
|
|
@@ -2792,7 +2890,7 @@ export const createGateway = (options = {}) => {
|
|
|
2792
2890
|
if (record) {
|
|
2793
2891
|
trackTurn(sessionId, controller);
|
|
2794
2892
|
if (abortingWith !== undefined) {
|
|
2795
|
-
controller.abort(
|
|
2893
|
+
controller.abort(shutdownAbort());
|
|
2796
2894
|
}
|
|
2797
2895
|
}
|
|
2798
2896
|
try {
|
|
@@ -3156,7 +3254,21 @@ export const createGateway = (options = {}) => {
|
|
|
3156
3254
|
if (stopping) {
|
|
3157
3255
|
throw refusal();
|
|
3158
3256
|
}
|
|
3159
|
-
return onSessionChain(input.sessionId, () =>
|
|
3257
|
+
return onSessionChain(input.sessionId, async () => {
|
|
3258
|
+
// A keyed firing (an event trigger's batch) that the session already
|
|
3259
|
+
// holds is that turn, not a new one: a daemon that died after the
|
|
3260
|
+
// turn and before the firing was recorded re-sends it at the next
|
|
3261
|
+
// start, and must answer with the finished turn (or settle the
|
|
3262
|
+
// unfinished one, as the start-up sweep would) instead of running the
|
|
3263
|
+
// event again. The same check the public door makes, on the chain.
|
|
3264
|
+
if (input.idempotencyKey !== undefined) {
|
|
3265
|
+
const repeated = await settleRepeat(input.sessionId, input.idempotencyKey, input.agentId, input.turnId, input.onRepeat);
|
|
3266
|
+
if (repeated !== undefined) {
|
|
3267
|
+
return repeated;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return dispatchInternal(input);
|
|
3271
|
+
});
|
|
3160
3272
|
};
|
|
3161
3273
|
const dispatch = async (input) => {
|
|
3162
3274
|
if (stopping) {
|
|
@@ -3948,6 +4060,7 @@ export const createGateway = (options = {}) => {
|
|
|
3948
4060
|
}
|
|
3949
4061
|
storesClosed = true;
|
|
3950
4062
|
scheduleStore.close();
|
|
4063
|
+
eventStore?.close();
|
|
3951
4064
|
store.close();
|
|
3952
4065
|
};
|
|
3953
4066
|
/**
|
|
@@ -4085,12 +4198,15 @@ export const createGateway = (options = {}) => {
|
|
|
4085
4198
|
// The watchdog's own mechanism: the runner fails each session with
|
|
4086
4199
|
// the reason on the signal, saves it, and emits session.failed —
|
|
4087
4200
|
// so the next daemon finds a finished turn, not an abandoned one.
|
|
4201
|
+
// Except a keyed turn, which the resumable abort leaves running for
|
|
4202
|
+
// the next daemon to continue once, as a crash would have left it
|
|
4203
|
+
// (`shutdownAbort`); its caller is refused (`leftForNext`).
|
|
4088
4204
|
// Before the loop, so a turn queued behind one aborted here, which
|
|
4089
4205
|
// starts as soon as that one settles, is aborted the same way.
|
|
4090
4206
|
abortingWith = kind === 'stop' ? STOPPING_TURN_ERROR : RESTARTING_TURN_ERROR;
|
|
4091
4207
|
for (const running of turnControllers.values()) {
|
|
4092
4208
|
for (const controller of running) {
|
|
4093
|
-
controller.abort(
|
|
4209
|
+
controller.abort(shutdownAbort());
|
|
4094
4210
|
}
|
|
4095
4211
|
}
|
|
4096
4212
|
drained = await settlesWithin(drain, remaining());
|
|
@@ -4425,6 +4541,29 @@ export const createGateway = (options = {}) => {
|
|
|
4425
4541
|
cancelSchedule(scheduleId) {
|
|
4426
4542
|
return scheduler.cancel(scheduleId);
|
|
4427
4543
|
},
|
|
4544
|
+
async receiveEvent(request) {
|
|
4545
|
+
if (!eventIntake) {
|
|
4546
|
+
return { status: 404, body: { error: 'unknown_source', message: 'No event source is configured at this path.' } };
|
|
4547
|
+
}
|
|
4548
|
+
const answer = await eventIntake.receive(request);
|
|
4549
|
+
// The delivery and its firings are committed; send them now rather
|
|
4550
|
+
// than at the next tick. The scheduler decides hold or fire, and does
|
|
4551
|
+
// nothing while it is stopped or not yet started.
|
|
4552
|
+
if (answer.body.status === 'matched') {
|
|
4553
|
+
scheduler.kickEvents();
|
|
4554
|
+
}
|
|
4555
|
+
return answer;
|
|
4556
|
+
},
|
|
4557
|
+
eventSummary() {
|
|
4558
|
+
const since = new Date(Date.now() - 86_400_000).toISOString();
|
|
4559
|
+
if (!eventStore || !eventIntake) {
|
|
4560
|
+
return { since, sources: {}, unknownSource: 0 };
|
|
4561
|
+
}
|
|
4562
|
+
return summarizeEvents({ sourceIds: Object.keys(eventSources), since, activity: eventStore.activity(since), counters: eventIntake.counters() });
|
|
4563
|
+
},
|
|
4564
|
+
eventDeliveries(options = {}) {
|
|
4565
|
+
return eventStore?.deliveries(options) ?? [];
|
|
4566
|
+
},
|
|
4428
4567
|
tools() {
|
|
4429
4568
|
// From the registry, not from the load records: the risk reported is
|
|
4430
4569
|
// the one a call will actually be judged at, floor applied, and a
|