@stratusagent/gateway 0.11.4 → 0.11.6
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/README.md +5 -1
- package/dist/index.d.ts +25 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -197
- package/dist/index.js.map +1 -1
- package/dist/schedules.d.ts +14 -1
- package/dist/schedules.d.ts.map +1 -1
- package/dist/schedules.js +5 -15
- package/dist/schedules.js.map +1 -1
- package/dist/sessions.d.ts +225 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +598 -0
- package/dist/sessions.js.map +1 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
One gateway owns:
|
|
6
6
|
|
|
7
|
-
- **Durable sessions** — `SqliteSessionStore`
|
|
7
|
+
- **Durable sessions, one database per agent** — `ShardedSessionStore` over a `SqliteSessionStore` per agent (`node:sqlite`, Node 22.13+, no native deps) at `~/.stratus/agents/<id>/sessions.db`, so a store is opened on one agent's path and no query can return another's rows. The whole session round-trips as one JSON body, provider replay state included, so a conversation resumed after a daemon restart replays exactly. Session ids are caller-chosen and stable (channels derive them from conversations), which is what makes any inbound message resumable — and a fleet-wide index in `~/.stratus/fleet.db` is where such an id is claimed, where a lookup with no agent in hand resolves it, and what answers the reads that were never about one agent (status counts, per-agent activity, the listing, the restart sweeps). `start()` reconciles the index against the stores before anything reads either.
|
|
8
8
|
- **A per-provider runner pool** — each agent's soul pins its own provider/model; config re-resolves on every dispatch and runners are pooled by resolved configuration, so two agents never share a billing path or credentials by accident.
|
|
9
9
|
- **Dispatcher-backed delegation** — `agent.delegate` routes the target through the same per-provider dispatch as a direct message; a delegated specialist runs on *their* provider, never the delegator's.
|
|
10
10
|
- **Approval recovery** — a turn parked on a human is a durable checkpoint, not a lost turn. The runner records the parked call and the calls queued behind it *before* asking, and clears it before the tool runs, so the record covers exactly the window in which nothing has happened. On start the gateway sweeps for `pending_approval` sessions and finishes them: the parked call is re-asked (or refused, if its window ran out while the process was down), and the queue behind it drains, so every `tool_use` still ends with a `tool_result`. Earlier calls are never replayed — their results were already durable, and a re-asked request keeps the remainder of its original window rather than starting a new one. Calls made through a provider that drives its own inner loop are deliberately excluded: recovery re-enters the *kernel* loop and cannot rebuild that provider's, so those fail cleanly instead (see [04](../../docs/roadmap/04-agent-sdk-bridge.md)).
|
|
@@ -26,3 +26,7 @@ await gateway.stop(); // drains in-flight turns first
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Run it in the foreground with `stratus serve`. A launchd template for macOS lives in `launchd/com.stratuslabs.stratusd.plist`.
|
|
29
|
+
|
|
30
|
+
**A host that starts the gateway itself owns the state migration.** `stratus serve` claims the home (`claimHome`) and runs the pending migrations before it ever calls `createGateway`. A host doing its own wiring must do the same on a directory written by a build before the per-agent layout: `start()` refuses one that still holds a shared `sessions.db` or an `<id>.whitelist.json`, rather than opening empty stores over it and starting a second population beside every session, schedule, and grant already there. The message names the fix — claim the home, then `await runStateMigrations(env, { exclusive: true })` from `@stratusagent/state`. The gateway will not do it unasked: the move needs the directory to itself, and only the host knows whether it has it.
|
|
31
|
+
|
|
32
|
+
The question is asked about `stateDir` — the directory the stores were actually opened on — not about `~/.stratus`. A host that set its own `stateDir` is stranding *that* directory's sessions, and `stratus update` does not migrate a directory it does not know about; the refusal says so rather than pointing at a remedy that would not apply.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,88 +1,12 @@
|
|
|
1
|
-
import { EventBus, type AgentDefinition, type AlwaysMeans, type ApprovalAnswer, type ApprovalOutcome, type ApprovalPolicy, type ImageAttachment, type JsonObject, type Session, type
|
|
1
|
+
import { EventBus, type AgentDefinition, type AlwaysMeans, type ApprovalAnswer, type ApprovalOutcome, type ApprovalPolicy, type ImageAttachment, type JsonObject, type Session, type ToolRisk } from '@stratusagent/core';
|
|
2
2
|
import { type ParsedSoul, type ScheduleRecord } from '@stratusagent/agents';
|
|
3
3
|
import { type SchedulerLimits } from './schedules.ts';
|
|
4
|
+
import { ShardedSessionStore } from './sessions.ts';
|
|
4
5
|
export { claimHome, homeLockPath, HomeClaimedError, type HomeClaim, } from './lock.ts';
|
|
6
|
+
export { SqliteSessionStore, ShardedSessionStore, FleetSessionIndex, SessionIdTakenError, tightenSqliteFile, type SqliteSessionStoreOptions, type ShardedSessionStoreOptions, type SessionIndexRow, type SessionReconcileReport, } from './sessions.ts';
|
|
5
7
|
export { SqliteScheduleStore, createSchedulerRuntime, isScheduleSessionId, SCHEDULE_SESSION_ID_PREFIX, type ScheduleStoreLike, type SchedulerLimits, type SchedulerRuntime, type SchedulerRuntimeOptions, } from './schedules.ts';
|
|
6
8
|
import { type OptionalModuleHost } from '@stratusagent/plugins';
|
|
7
9
|
import { type PluginsConfig, type RuntimeSelection, type StateEnvironment } from '@stratusagent/state';
|
|
8
|
-
/**
|
|
9
|
-
* Where a daemon with no explicit `sessionDbPath` keeps its database — the
|
|
10
|
-
* one rule for it, exported because `stratus schedules` opens the same file
|
|
11
|
-
* from another process and a re-derived copy of this join is how the CLI
|
|
12
|
-
* ends up auditing a database no daemon writes.
|
|
13
|
-
*/
|
|
14
|
-
export declare const defaultSessionDbPath: (env?: StateEnvironment) => string;
|
|
15
|
-
/**
|
|
16
|
-
* Durable session storage on node:sqlite (unflagged on Node 22.13+). The
|
|
17
|
-
* whole session — messages, status, and metadata, including provider replay
|
|
18
|
-
* state like the Anthropic raw-turn cache — round-trips as one JSON body,
|
|
19
|
-
* so a conversation resumed after a daemon restart replays exactly.
|
|
20
|
-
*/
|
|
21
|
-
export interface SqliteSessionStoreOptions {
|
|
22
|
-
/**
|
|
23
|
-
* The database's parent directory is dedicated Stratus state (e.g. the
|
|
24
|
-
* default ~/.stratus): tighten it to owner-only even when it already
|
|
25
|
-
* exists, since mkdir's mode only applies to directories it creates and
|
|
26
|
-
* an upgrade over a looser install must not stay world-readable. Leave
|
|
27
|
-
* false for caller-supplied paths — a shared parent like /tmp or a
|
|
28
|
-
* project directory must never be chmodded implicitly.
|
|
29
|
-
*/
|
|
30
|
-
ownedDirectory?: boolean;
|
|
31
|
-
}
|
|
32
|
-
export declare class SqliteSessionStore implements SessionStore {
|
|
33
|
-
private readonly db;
|
|
34
|
-
constructor(filePath: string, options?: SqliteSessionStoreOptions);
|
|
35
|
-
create(input: Omit<Session, 'createdAt' | 'updatedAt'>): Promise<Session>;
|
|
36
|
-
get(id: string): Promise<Session | undefined>;
|
|
37
|
-
save(session: Session): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Session ids in a state, oldest first — the index a restarting daemon
|
|
40
|
-
* sweeps for turns parked on a human. The `status` column carries it, so
|
|
41
|
-
* no conversation body is deserialized to answer the question.
|
|
42
|
-
*/
|
|
43
|
-
listIdsByStatus(status: SessionStatus): Promise<string[]>;
|
|
44
|
-
/**
|
|
45
|
-
* When each agent last did anything, and how many of its sessions are live
|
|
46
|
-
* right now.
|
|
47
|
-
*
|
|
48
|
-
* One aggregate over the indexed columns — no conversation body is
|
|
49
|
-
* deserialized, for the same reason `listIdsByStatus` returns ids: a
|
|
50
|
-
* roster view asks this on every load, and it must not cost a JSON parse
|
|
51
|
-
* per session to answer.
|
|
52
|
-
*
|
|
53
|
-
* Both halves are needed and neither is sufficient. `lastActiveAt` alone
|
|
54
|
-
* reads a turn parked on a human as idle, because the save that recorded
|
|
55
|
-
* the park is the last thing that touched the row — and a turn waiting
|
|
56
|
-
* twenty minutes on an approval is exactly when someone wants to see the
|
|
57
|
-
* agent lit. `activeSessions` alone loses an agent that finished a moment
|
|
58
|
-
* ago. The caller decides what window counts as "recent"; a daemon that
|
|
59
|
-
* baked one in would need upgrading to change it.
|
|
60
|
-
*/
|
|
61
|
-
/**
|
|
62
|
-
* How many sessions are in each state.
|
|
63
|
-
*
|
|
64
|
-
* A grouped count, not a listing that gets counted. The table grows for the
|
|
65
|
-
* life of an install and a health endpoint is polled, so materialising one
|
|
66
|
-
* object per historical session to produce five numbers gets steadily
|
|
67
|
-
* slower at exactly the thing meant to report that the daemon is fine.
|
|
68
|
-
*/
|
|
69
|
-
countByStatus(): Record<string, number>;
|
|
70
|
-
lastActivityByAgent(): Record<string, {
|
|
71
|
-
lastActiveAt: string;
|
|
72
|
-
activeSessions: number;
|
|
73
|
-
}>;
|
|
74
|
-
/**
|
|
75
|
-
* Newest-first session listing for one agent (or all agents).
|
|
76
|
-
*
|
|
77
|
-
* `limit` is not decoration: this table grows for the life of an install,
|
|
78
|
-
* and a surface that renders "recent conversations" would otherwise pull
|
|
79
|
-
* every session anyone has ever had to show ten of them.
|
|
80
|
-
*/
|
|
81
|
-
list(agentId?: string, limit?: number): Array<Pick<Session, 'id' | 'status' | 'createdAt' | 'updatedAt'> & {
|
|
82
|
-
agentId: string;
|
|
83
|
-
}>;
|
|
84
|
-
close(): void;
|
|
85
|
-
}
|
|
86
10
|
/**
|
|
87
11
|
* A soul's provider/model pins, and the daemon-wide defaults they demote.
|
|
88
12
|
*
|
|
@@ -409,6 +333,16 @@ export interface GatewayOptions {
|
|
|
409
333
|
* which is only ever right in a test. Default 15 minutes.
|
|
410
334
|
*/
|
|
411
335
|
approvalTimeoutMs?: number;
|
|
336
|
+
/**
|
|
337
|
+
* How many tool turns one dispatched turn may take before the runner
|
|
338
|
+
* wraps it up with a summary. Default 40 (`DEFAULT_MAX_TURNS` in core).
|
|
339
|
+
*
|
|
340
|
+
* Applies to every runner this gateway builds, so a delegated
|
|
341
|
+
* sub-session is held to the same ceiling as the turn that delegated it
|
|
342
|
+
* — they are separate dispatches and each gets its own allowance, not a
|
|
343
|
+
* share of one. `stratus serve` fills it from the trusted config's
|
|
344
|
+
* `maxTurns`.
|
|
345
|
+
*/
|
|
412
346
|
maxTurns?: number;
|
|
413
347
|
/**
|
|
414
348
|
* The activity watchdog: abort a turn when no event for its session has
|
|
@@ -416,8 +350,17 @@ export interface GatewayOptions {
|
|
|
416
350
|
* event, or response resets it. 0 disables. Default 120s.
|
|
417
351
|
*/
|
|
418
352
|
idleTimeoutMs?: number;
|
|
419
|
-
/**
|
|
420
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Where the per-agent session stores (`agents/<id>/sessions.db`) and the
|
|
355
|
+
* fleet database (`fleet.db`, holding the schedules and the session
|
|
356
|
+
* index) live. Default `~/.stratus`.
|
|
357
|
+
*
|
|
358
|
+
* A directory rather than the single database path it replaced: after
|
|
359
|
+
* step 15's layer A there is no one file to point at, which is the
|
|
360
|
+
* point — a store is opened on one agent's path, so no query can return
|
|
361
|
+
* another agent's rows.
|
|
362
|
+
*/
|
|
363
|
+
stateDir?: string;
|
|
421
364
|
/**
|
|
422
365
|
* Scheduler limits — the interval floor, the per-agent cap on concurrent
|
|
423
366
|
* scheduled turns, and the tick cadence. Defaults are production values;
|
|
@@ -542,7 +485,7 @@ export interface Gateway {
|
|
|
542
485
|
/** Live events from every runner, one stream for all consumers. */
|
|
543
486
|
readonly bus: EventBus;
|
|
544
487
|
/** The store shared by every runner (durable across restarts). */
|
|
545
|
-
readonly store:
|
|
488
|
+
readonly store: ShardedSessionStore;
|
|
546
489
|
/** The current roster, default agent included. */
|
|
547
490
|
agents(): AgentDefinition[];
|
|
548
491
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -7,25 +7,16 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
13
|
-
import { filePathsOf, isUnaddressedTurn, abortErrorFor, AgentRegistry, AgentRunner, ChannelRegistry, ContributionRegistry, createRoutedMemoryStore, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, PENDING_APPROVAL_METADATA_KEY, SESSION_TAINTED_BY_METADATA_KEY, SESSION_TRUST_METADATA_KEY, latestTurnReply, readPendingApproval, } from '@stratusagent/core';
|
|
14
|
-
import { createDelegateTool, createForgetTool, createMessageSendTool, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, } from '@stratusagent/agents';
|
|
10
|
+
import { filePathsOf, isUnaddressedTurn, abortErrorFor, 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, } from '@stratusagent/core';
|
|
11
|
+
import { createDelegateTool, createForgetTool, createPinTool, createMessageSendTool, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, } from '@stratusagent/agents';
|
|
15
12
|
import { createSchedulerRuntime, isScheduleSessionId, SqliteScheduleStore, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
13
|
+
import { ShardedSessionStore } from "./sessions.js";
|
|
16
14
|
export { claimHome, homeLockPath, HomeClaimedError, } from "./lock.js";
|
|
15
|
+
export { SqliteSessionStore, ShardedSessionStore, FleetSessionIndex, SessionIdTakenError, tightenSqliteFile, } from "./sessions.js";
|
|
17
16
|
export { SqliteScheduleStore, createSchedulerRuntime, isScheduleSessionId, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
18
17
|
import { createLocalCommandExecutor } from '@stratusagent/executor-local';
|
|
19
18
|
import { loadPlugins, } from '@stratusagent/plugins';
|
|
20
|
-
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver,
|
|
21
|
-
const SESSIONS_DB_FILENAME = 'sessions.db';
|
|
22
|
-
/**
|
|
23
|
-
* Where a daemon with no explicit `sessionDbPath` keeps its database — the
|
|
24
|
-
* one rule for it, exported because `stratus schedules` opens the same file
|
|
25
|
-
* from another process and a re-derived copy of this join is how the CLI
|
|
26
|
-
* ends up auditing a database no daemon writes.
|
|
27
|
-
*/
|
|
28
|
-
export const defaultSessionDbPath = (env = {}) => path.join(stratusHomePath(env), SESSIONS_DB_FILENAME);
|
|
19
|
+
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver, createHomeMemoryStore, drainSharedMemory, hasBracketedLegacyStateIn, createRuntimeProvider, DEFAULT_STRATUS_AGENT, isRegisteredProviderName, loadChannelTransportSecrets, registeredProviderNameOf, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, migrateLegacyMemory, PROVIDER_STATE_METADATA_KEYS, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, applySoulPins, assertStateCompatible, stratusHomePath, fleetDbIn, createAgentWorkspaces, foldedAgentId, } from '@stratusagent/state';
|
|
29
20
|
const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
|
|
30
21
|
/**
|
|
31
22
|
* How long a parked call waits for a person. Long enough to survive a
|
|
@@ -39,179 +30,6 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 900_000;
|
|
|
39
30
|
* 30-day approval window would expire every request almost immediately.
|
|
40
31
|
*/
|
|
41
32
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
42
|
-
export class SqliteSessionStore {
|
|
43
|
-
db;
|
|
44
|
-
constructor(filePath, options = {}) {
|
|
45
|
-
// Sessions hold complete conversations (prompts, replies, tool output,
|
|
46
|
-
// provider replay state) — owner-only, like the credentials file. The
|
|
47
|
-
// file chmods below cover databases created earlier under a looser
|
|
48
|
-
// umask too; directories the store creates are born 0700.
|
|
49
|
-
const dir = path.dirname(filePath);
|
|
50
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
51
|
-
if (options.ownedDirectory) {
|
|
52
|
-
chmodSync(dir, 0o700);
|
|
53
|
-
}
|
|
54
|
-
this.db = new DatabaseSync(filePath);
|
|
55
|
-
// The database file is tightened FIRST: SQLite derives sidecar
|
|
56
|
-
// permissions from the main file's mode, so everything created later
|
|
57
|
-
// inherits owner-only.
|
|
58
|
-
try {
|
|
59
|
-
chmodSync(filePath, 0o600);
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
// A database that cannot be tightened must not be used: conversation
|
|
63
|
-
// bodies would stay readable by other local users for the daemon's
|
|
64
|
-
// whole lifetime, silently.
|
|
65
|
-
if (error.code !== 'ENOENT') {
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// WAL, not the default DELETE journal: DELETE mode recreates a
|
|
70
|
-
// rollback journal on EVERY write, which a one-time chmod could never
|
|
71
|
-
// cover in a traversable caller-supplied directory. WAL keeps one
|
|
72
|
-
// persistent sidecar pair per connection — forced into existence here
|
|
73
|
-
// (the user_version pragma is a real page-one write) so the loop
|
|
74
|
-
// below covers them for the connection's whole lifetime.
|
|
75
|
-
this.db.exec('PRAGMA journal_mode = WAL');
|
|
76
|
-
// A busy timeout, because this file has a second writer: `stratus
|
|
77
|
-
// schedules cancel` opens its own connection to the schedules table
|
|
78
|
-
// living here, and WAL serializes writers file-wide — a session save
|
|
79
|
-
// racing that delete would otherwise throw `database is locked` rather
|
|
80
|
-
// than wait its brief turn.
|
|
81
|
-
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
82
|
-
this.db.exec(`
|
|
83
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
84
|
-
id TEXT PRIMARY KEY,
|
|
85
|
-
agent_id TEXT NOT NULL,
|
|
86
|
-
status TEXT NOT NULL,
|
|
87
|
-
body TEXT NOT NULL,
|
|
88
|
-
created_at TEXT NOT NULL,
|
|
89
|
-
updated_at TEXT NOT NULL
|
|
90
|
-
)
|
|
91
|
-
`);
|
|
92
|
-
this.db.exec('PRAGMA user_version = 0');
|
|
93
|
-
for (const sensitive of [filePath, `${filePath}-wal`, `${filePath}-shm`, `${filePath}-journal`]) {
|
|
94
|
-
try {
|
|
95
|
-
chmodSync(sensitive, 0o600);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
// A sidecar that does not exist has nothing to tighten; anything
|
|
99
|
-
// else means session data stays readable — refuse to run over it.
|
|
100
|
-
if (error.code !== 'ENOENT') {
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
async create(input) {
|
|
107
|
-
const now = new Date().toISOString();
|
|
108
|
-
const session = { ...input, createdAt: now, updatedAt: now };
|
|
109
|
-
this.db
|
|
110
|
-
.prepare('INSERT OR REPLACE INTO sessions (id, agent_id, status, body, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
111
|
-
.run(session.id, session.agent.id, session.status, JSON.stringify(session), session.createdAt, session.updatedAt);
|
|
112
|
-
return session;
|
|
113
|
-
}
|
|
114
|
-
async get(id) {
|
|
115
|
-
const row = this.db.prepare('SELECT body FROM sessions WHERE id = ?').get(id);
|
|
116
|
-
return row ? JSON.parse(row.body) : undefined;
|
|
117
|
-
}
|
|
118
|
-
async save(session) {
|
|
119
|
-
const updated = { ...session, updatedAt: new Date().toISOString() };
|
|
120
|
-
this.db
|
|
121
|
-
.prepare('INSERT OR REPLACE INTO sessions (id, agent_id, status, body, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
122
|
-
.run(updated.id, updated.agent.id, updated.status, JSON.stringify(updated), updated.createdAt, updated.updatedAt);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Session ids in a state, oldest first — the index a restarting daemon
|
|
126
|
-
* sweeps for turns parked on a human. The `status` column carries it, so
|
|
127
|
-
* no conversation body is deserialized to answer the question.
|
|
128
|
-
*/
|
|
129
|
-
async listIdsByStatus(status) {
|
|
130
|
-
const rows = this.db
|
|
131
|
-
.prepare('SELECT id FROM sessions WHERE status = ? ORDER BY updated_at ASC')
|
|
132
|
-
.all(status);
|
|
133
|
-
return rows.map((row) => row.id);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* When each agent last did anything, and how many of its sessions are live
|
|
137
|
-
* right now.
|
|
138
|
-
*
|
|
139
|
-
* One aggregate over the indexed columns — no conversation body is
|
|
140
|
-
* deserialized, for the same reason `listIdsByStatus` returns ids: a
|
|
141
|
-
* roster view asks this on every load, and it must not cost a JSON parse
|
|
142
|
-
* per session to answer.
|
|
143
|
-
*
|
|
144
|
-
* Both halves are needed and neither is sufficient. `lastActiveAt` alone
|
|
145
|
-
* reads a turn parked on a human as idle, because the save that recorded
|
|
146
|
-
* the park is the last thing that touched the row — and a turn waiting
|
|
147
|
-
* twenty minutes on an approval is exactly when someone wants to see the
|
|
148
|
-
* agent lit. `activeSessions` alone loses an agent that finished a moment
|
|
149
|
-
* ago. The caller decides what window counts as "recent"; a daemon that
|
|
150
|
-
* baked one in would need upgrading to change it.
|
|
151
|
-
*/
|
|
152
|
-
/**
|
|
153
|
-
* How many sessions are in each state.
|
|
154
|
-
*
|
|
155
|
-
* A grouped count, not a listing that gets counted. The table grows for the
|
|
156
|
-
* life of an install and a health endpoint is polled, so materialising one
|
|
157
|
-
* object per historical session to produce five numbers gets steadily
|
|
158
|
-
* slower at exactly the thing meant to report that the daemon is fine.
|
|
159
|
-
*/
|
|
160
|
-
countByStatus() {
|
|
161
|
-
const rows = this.db
|
|
162
|
-
.prepare('SELECT status, COUNT(*) AS total FROM sessions GROUP BY status')
|
|
163
|
-
.all();
|
|
164
|
-
const counts = {};
|
|
165
|
-
for (const row of rows) {
|
|
166
|
-
counts[row.status] = Number(row.total);
|
|
167
|
-
}
|
|
168
|
-
return counts;
|
|
169
|
-
}
|
|
170
|
-
lastActivityByAgent() {
|
|
171
|
-
const rows = this.db
|
|
172
|
-
.prepare(`
|
|
173
|
-
SELECT agent_id,
|
|
174
|
-
MAX(updated_at) AS last_active_at,
|
|
175
|
-
SUM(CASE WHEN status IN ('running', 'pending_approval') THEN 1 ELSE 0 END) AS active_sessions
|
|
176
|
-
FROM sessions
|
|
177
|
-
GROUP BY agent_id
|
|
178
|
-
`)
|
|
179
|
-
.all();
|
|
180
|
-
const activity = {};
|
|
181
|
-
for (const row of rows) {
|
|
182
|
-
activity[row.agent_id] = {
|
|
183
|
-
lastActiveAt: row.last_active_at,
|
|
184
|
-
activeSessions: Number(row.active_sessions),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return activity;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Newest-first session listing for one agent (or all agents).
|
|
191
|
-
*
|
|
192
|
-
* `limit` is not decoration: this table grows for the life of an install,
|
|
193
|
-
* and a surface that renders "recent conversations" would otherwise pull
|
|
194
|
-
* every session anyone has ever had to show ten of them.
|
|
195
|
-
*/
|
|
196
|
-
list(agentId, limit) {
|
|
197
|
-
// -1 is SQLite's "no limit", so one prepared statement serves both cases
|
|
198
|
-
// rather than four.
|
|
199
|
-
const bound = limit !== undefined && Number.isInteger(limit) && limit >= 0 ? limit : -1;
|
|
200
|
-
const rows = (agentId
|
|
201
|
-
? this.db.prepare('SELECT id, agent_id, status, created_at, updated_at FROM sessions WHERE agent_id = ? ORDER BY updated_at DESC LIMIT ?').all(agentId, bound)
|
|
202
|
-
: this.db.prepare('SELECT id, agent_id, status, created_at, updated_at FROM sessions ORDER BY updated_at DESC LIMIT ?').all(bound));
|
|
203
|
-
return rows.map((row) => ({
|
|
204
|
-
id: row.id,
|
|
205
|
-
agentId: row.agent_id,
|
|
206
|
-
status: row.status,
|
|
207
|
-
createdAt: row.created_at,
|
|
208
|
-
updatedAt: row.updated_at,
|
|
209
|
-
}));
|
|
210
|
-
}
|
|
211
|
-
close() {
|
|
212
|
-
this.db.close();
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
33
|
/**
|
|
216
34
|
* A soul's provider/model pins, and the daemon-wide defaults they demote.
|
|
217
35
|
*
|
|
@@ -308,6 +126,13 @@ export const RESERVED_SESSION_METADATA_KEYS = [
|
|
|
308
126
|
// where a command ran, and the one audit question a caller must not be
|
|
309
127
|
// able to answer for it.
|
|
310
128
|
EXECUTOR_METADATA_KEY,
|
|
129
|
+
// The context floor is an index into a specific transcript, so it means
|
|
130
|
+
// nothing next to a different one. Reserved for both halves of that: a
|
|
131
|
+
// caller cannot seed it, and a rollover drops it with the transcript it
|
|
132
|
+
// indexed — otherwise the fresh session inherits an absolute floor over
|
|
133
|
+
// no messages and, once it grows past that index, silently stops sending
|
|
134
|
+
// its own earliest turns, with no overflow and no trim event to say so.
|
|
135
|
+
CONTEXT_FLOOR_METADATA_KEY,
|
|
311
136
|
];
|
|
312
137
|
/**
|
|
313
138
|
* The first reserved key `metadata` carries, or undefined when it carries
|
|
@@ -332,20 +157,21 @@ export const createGateway = (options = {}) => {
|
|
|
332
157
|
const bus = new EventBus({
|
|
333
158
|
onError: (error) => warn(`event handler failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
334
159
|
});
|
|
335
|
-
// The default
|
|
336
|
-
// tightened to owner-only; a caller-supplied
|
|
160
|
+
// The default state directory is the gateway-owned ~/.stratus, which is
|
|
161
|
+
// tightened to owner-only; a caller-supplied one may sit in a shared
|
|
337
162
|
// directory that must not be chmodded from under other processes.
|
|
338
|
-
const
|
|
339
|
-
const store =
|
|
340
|
-
|
|
341
|
-
|
|
163
|
+
const stateDir = options.stateDir ?? stratusHomePath(env);
|
|
164
|
+
const store = new ShardedSessionStore({
|
|
165
|
+
stateDir,
|
|
166
|
+
...(options.stateDir ? {} : { ownedDirectory: true }),
|
|
167
|
+
});
|
|
342
168
|
// The built-in store, behind a route that can change its mind: the memory
|
|
343
169
|
// tools and every runner are built before a plugin has loaded, so the
|
|
344
170
|
// store they hold is the one that answers *per call* with whichever was
|
|
345
171
|
// selected once the plugins are up. Per call, not per start, is also
|
|
346
172
|
// what keeps a contributed store per-agent-resolvable rather than
|
|
347
173
|
// process-global.
|
|
348
|
-
const fileMemory =
|
|
174
|
+
const fileMemory = createHomeMemoryStore(env);
|
|
349
175
|
let selectedMemory = fileMemory;
|
|
350
176
|
const memory = createRoutedMemoryStore(() => selectedMemory);
|
|
351
177
|
let selectedExecutor;
|
|
@@ -363,7 +189,7 @@ export const createGateway = (options = {}) => {
|
|
|
363
189
|
// ---- schedules ----------------------------------------------------------
|
|
364
190
|
// The same database sessions live in — one file to back up — through the
|
|
365
191
|
// schedule store's own connection (see its docs for why).
|
|
366
|
-
const scheduleStore = new SqliteScheduleStore(
|
|
192
|
+
const scheduleStore = new SqliteScheduleStore(fleetDbIn(stateDir), { stateHome: stateDir });
|
|
367
193
|
/**
|
|
368
194
|
* The write side of an addressable destination, through whichever
|
|
369
195
|
* started channel serves its kind. Resolution happens per call against
|
|
@@ -706,6 +532,24 @@ export const createGateway = (options = {}) => {
|
|
|
706
532
|
registry.register(source.definition);
|
|
707
533
|
sources.set(source.definition.id, source);
|
|
708
534
|
};
|
|
535
|
+
/**
|
|
536
|
+
* An id already registered that would open `agentId`'s state directory
|
|
537
|
+
* while being a different id — `ava` when asked about `Ava`, on the
|
|
538
|
+
* filesystems that fold them together.
|
|
539
|
+
*
|
|
540
|
+
* `foldedAgentId` owns which pairs those are. What this adds is the
|
|
541
|
+
* "already registered" half, for the one registration that does not come
|
|
542
|
+
* through `loadRosterSouls` and so was never folded against anything.
|
|
543
|
+
*/
|
|
544
|
+
const stateDirectoryPeer = (agentId) => {
|
|
545
|
+
const folded = foldedAgentId(agentId);
|
|
546
|
+
for (const other of sources.keys()) {
|
|
547
|
+
if (other !== agentId && foldedAgentId(other) === folded) {
|
|
548
|
+
return other;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
};
|
|
709
553
|
/**
|
|
710
554
|
* The agent an agentId-less dispatch routes to: the configured default
|
|
711
555
|
* soul when one is set (what `stratus setup` writes to config.json),
|
|
@@ -762,8 +606,30 @@ export const createGateway = (options = {}) => {
|
|
|
762
606
|
lastDefaultResolved = undefined;
|
|
763
607
|
return DEFAULT_STRATUS_AGENT.id;
|
|
764
608
|
}
|
|
765
|
-
lastDefaultResolved = resolved;
|
|
766
609
|
const id = resolved.soul.agent.id;
|
|
610
|
+
// The roster refuses two *souls* whose ids name one state directory,
|
|
611
|
+
// but this soul never went through it: a config-only default is
|
|
612
|
+
// resolved here, by path, and registered by exact id. So `Ava` in
|
|
613
|
+
// config beside `ava` on the roster — or `Stratus` beside the built-in
|
|
614
|
+
// — passed as two agents sharing one `agents/<id>/`, which is one
|
|
615
|
+
// `whitelist.json` and therefore one agent acting on the other's
|
|
616
|
+
// unattended grants.
|
|
617
|
+
//
|
|
618
|
+
// Warned and dropped rather than thrown: this runs on every
|
|
619
|
+
// agentId-less dispatch, and a configuration mistake must not turn
|
|
620
|
+
// every such turn into an error. Dropping routes to the built-in,
|
|
621
|
+
// which is the documented answer when there is no usable default, and
|
|
622
|
+
// leaves the colliding agent serving from its own directory alone.
|
|
623
|
+
const peer = stateDirectoryPeer(id);
|
|
624
|
+
if (peer !== undefined) {
|
|
625
|
+
warn(`the configured default soul at ${resolved.path} declares the agent id ${id}, which names the same `
|
|
626
|
+
+ `state directory as ${peer} — sessions, memories, and the grants that say what may run unattended. `
|
|
627
|
+
+ `Ignoring it and routing to ${DEFAULT_STRATUS_AGENT.id}; rename one of the two ids.`);
|
|
628
|
+
lastDefaultAgentId = DEFAULT_STRATUS_AGENT.id;
|
|
629
|
+
lastDefaultResolved = undefined;
|
|
630
|
+
return DEFAULT_STRATUS_AGENT.id;
|
|
631
|
+
}
|
|
632
|
+
lastDefaultResolved = resolved;
|
|
767
633
|
// The normal setup layout has the default soul in ~/.stratus/agents
|
|
768
634
|
// too: keep the roster registration — its soulPath drives per-dispatch
|
|
769
635
|
// refresh. A config-only soul registers with the path it resolved
|
|
@@ -1021,6 +887,7 @@ export const createGateway = (options = {}) => {
|
|
|
1021
887
|
tools.register(createRememberTool(memory));
|
|
1022
888
|
tools.register(createRecallTool(memory));
|
|
1023
889
|
tools.register(createForgetTool(memory));
|
|
890
|
+
tools.register(createPinTool(memory));
|
|
1024
891
|
// Registered here rather than left to the first runner, so `tools()`
|
|
1025
892
|
// lists the reader before anything dispatches. The allowlist resolver
|
|
1026
893
|
// mirrors the runner's own sourcing — the definition travelling with the
|
|
@@ -1985,7 +1852,7 @@ export const createGateway = (options = {}) => {
|
|
|
1985
1852
|
// *calling* agent's — its allowlist checked, its own entry before the
|
|
1986
1853
|
// fleet's shared one.
|
|
1987
1854
|
credentials: createFileCredentialResolver(env),
|
|
1988
|
-
|
|
1855
|
+
workspaces: createAgentWorkspaces(env),
|
|
1989
1856
|
// The structured log, so a plugin's lifecycle lines — an MCP server
|
|
1990
1857
|
// that dropped, a reconnect that failed — are in `stratus logs` and
|
|
1991
1858
|
// not only on a stderr the service manager owns.
|
|
@@ -2076,6 +1943,42 @@ export const createGateway = (options = {}) => {
|
|
|
2076
1943
|
// it is synchronous for the same reason: the check is a file read, and
|
|
2077
1944
|
// an async one in that stretch failed CI's restart tests twice.
|
|
2078
1945
|
assertStateCompatible(env);
|
|
1946
|
+
// A home whose pre-15a state has not been moved yet is refused rather
|
|
1947
|
+
// than served, because serving it is the quiet failure: the stores open
|
|
1948
|
+
// on the new paths, find nothing, and the fleet starts writing a second
|
|
1949
|
+
// population beside every session, schedule, and grant it already had.
|
|
1950
|
+
//
|
|
1951
|
+
// Refused rather than migrated, even though this process is the daemon:
|
|
1952
|
+
// the move needs the home to *itself*, and holding the claim is
|
|
1953
|
+
// something the host does (`claimHome`) — a gateway that migrated on
|
|
1954
|
+
// its own would be asserting an exclusivity nobody gave it. `stratus
|
|
1955
|
+
// serve` runs the migration before it ever gets here, so this only
|
|
1956
|
+
// fires for a host wiring the gateway up itself.
|
|
1957
|
+
// Against `stateDir` — the directory the stores were actually opened
|
|
1958
|
+
// on — rather than the one `env` names. A host that pointed them
|
|
1959
|
+
// somewhere else would otherwise be told its home is fine while the
|
|
1960
|
+
// sessions it is stranding sit in the directory it chose.
|
|
1961
|
+
if (await hasBracketedLegacyStateIn(stateDir)) {
|
|
1962
|
+
throw new Error(`${stateDir} still holds pre-per-agent state (a shared sessions.db, or an <id>.whitelist.json beside the souls), `
|
|
1963
|
+
+ 'and serving it now would strand every session, schedule, and grant in it. '
|
|
1964
|
+
+ (stateDir === stratusHomePath(env)
|
|
1965
|
+
? 'Run `stratus update`, or — for a host starting the gateway itself — claim the home with `claimHome` and '
|
|
1966
|
+
+ 'await `runStateMigrations(env, { exclusive: true })` from @stratusagent/state before calling start().'
|
|
1967
|
+
: 'This is a state directory of your own choosing, which `stratus update` does not migrate: move that state '
|
|
1968
|
+
+ 'into the layout yourself, or point `stateDir` at a directory that has been through it.'));
|
|
1969
|
+
}
|
|
1970
|
+
// Before the sweeps below read a single session id: the index and the
|
|
1971
|
+
// per-agent stores are two durable writes where the shared database had
|
|
1972
|
+
// one primary key, and a crash between them left them disagreeing.
|
|
1973
|
+
// Reconciling here is what makes that disagreement resolve one way —
|
|
1974
|
+
// a claim with no conversation released, a conversation the index lost
|
|
1975
|
+
// re-indexed — rather than leaving an abandoned turn unreachable by the
|
|
1976
|
+
// sweep that was supposed to fail it honestly.
|
|
1977
|
+
const reconciled = await store.reconcile();
|
|
1978
|
+
if (reconciled.released.length > 0 || reconciled.reindexed.length > 0) {
|
|
1979
|
+
log(`session index reconciled: ${reconciled.reindexed.length} re-indexed, `
|
|
1980
|
+
+ `${reconciled.released.length} claim(s) released`);
|
|
1981
|
+
}
|
|
2079
1982
|
await loadRoster();
|
|
2080
1983
|
const named = registry.list().map((agent) => agent.name).join(', ');
|
|
2081
1984
|
log(`stratusd ready — ${registry.list().length} agent(s): ${named}`);
|
|
@@ -2313,6 +2216,13 @@ export const createGateway = (options = {}) => {
|
|
|
2313
2216
|
async start() {
|
|
2314
2217
|
try {
|
|
2315
2218
|
await migrateLegacyMemory(env);
|
|
2219
|
+
// And anything still in the shared `memory.jsonl` a pre-15a build
|
|
2220
|
+
// left (or a still-running one of those recreated). Not a
|
|
2221
|
+
// migration and never stamped — see `drainSharedMemory`.
|
|
2222
|
+
const drained = await drainSharedMemory(env);
|
|
2223
|
+
if (drained) {
|
|
2224
|
+
log(`shared memory drained: ${drained}`);
|
|
2225
|
+
}
|
|
2316
2226
|
// Before the roster and before channels: a turn must never arrive
|
|
2317
2227
|
// for an agent whose soul lists a tool the daemon has not
|
|
2318
2228
|
// registered yet, which would refuse the call as "not permitted"
|