@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Session, SessionStatus, SessionStore } from '@stratusagent/core';
|
|
2
|
+
/**
|
|
3
|
+
* Sessions hold complete conversations (prompts, replies, tool output,
|
|
4
|
+
* provider replay state) — owner-only, like the credentials file. The file
|
|
5
|
+
* chmods cover databases created earlier under a looser umask too;
|
|
6
|
+
* directories the stores create are born 0700.
|
|
7
|
+
*
|
|
8
|
+
* Exported because the layout migration writes the same files from
|
|
9
|
+
* `@stratusagent/state` before any store is open, and a second hand-rolled
|
|
10
|
+
* chmod loop is how one of the two ends up leaving a sidecar readable.
|
|
11
|
+
*/
|
|
12
|
+
export declare const tightenSqliteFile: (filePath: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Durable session storage on node:sqlite (unflagged on Node 22.13+). The
|
|
15
|
+
* whole session — messages, status, and metadata, including provider replay
|
|
16
|
+
* state like the Anthropic raw-turn cache — round-trips as one JSON body,
|
|
17
|
+
* so a conversation resumed after a daemon restart replays exactly.
|
|
18
|
+
*/
|
|
19
|
+
export interface SqliteSessionStoreOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The state home this file sits under, when it is one Stratus derived —
|
|
22
|
+
* `~/.stratus`, or whatever `--state-dir` made the home. Given it, the
|
|
23
|
+
* parent directory is tightened to owner-only even when it already exists
|
|
24
|
+
* (mkdir's mode only applies to what it creates, and an upgrade over a
|
|
25
|
+
* looser install must not stay world-readable) and every component below
|
|
26
|
+
* the home is required to be real, through `assertDerivedStatePath`.
|
|
27
|
+
*
|
|
28
|
+
* Left out for a caller-supplied path: a shared parent like `/tmp` or a
|
|
29
|
+
* project directory must never be chmodded implicitly, and its layout is
|
|
30
|
+
* not ours to have opinions about.
|
|
31
|
+
*
|
|
32
|
+
* Replaces `perAgent`, which approximated "did Stratus derive this path"
|
|
33
|
+
* and got the split wrong: the home is the *operator's* path and may be a
|
|
34
|
+
* link to another disk, so it is followed, while everything below it is a
|
|
35
|
+
* name Stratus chose, where nothing legitimate is a link. Naming the home
|
|
36
|
+
* says which is which, and says it about every component rather than the
|
|
37
|
+
* last one — so `fleet.db`, which lives directly in the home and was
|
|
38
|
+
* checked by neither boolean, is covered like any other derived name.
|
|
39
|
+
*/
|
|
40
|
+
stateHome?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Whether `stateHome` is itself Stratus's to tighten — true for the
|
|
43
|
+
* default `~/.stratus`, false for a state directory a caller supplied,
|
|
44
|
+
* which may sit in a shared parent that must not be chmodded from under
|
|
45
|
+
* whoever else uses it.
|
|
46
|
+
*
|
|
47
|
+
* Only about that one directory. Everything *below* the home is a
|
|
48
|
+
* directory Stratus created and is tightened either way, which is why
|
|
49
|
+
* this is not the same question as `stateHome` and cannot be folded into
|
|
50
|
+
* it: a caller-supplied state directory still gets `agents/<id>/` at
|
|
51
|
+
* `0700`, and still refuses a link there.
|
|
52
|
+
*/
|
|
53
|
+
ownedHome?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare class SqliteSessionStore implements SessionStore {
|
|
56
|
+
private readonly db;
|
|
57
|
+
constructor(filePath: string, options?: SqliteSessionStoreOptions);
|
|
58
|
+
private write;
|
|
59
|
+
create(input: Omit<Session, 'createdAt' | 'updatedAt'>): Promise<Session>;
|
|
60
|
+
get(id: string): Promise<Session | undefined>;
|
|
61
|
+
save(session: Session): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* The same save, handing back the row as it was written.
|
|
64
|
+
*
|
|
65
|
+
* For a caller that has to mirror it: the sharded store keeps the fleet
|
|
66
|
+
* index in step, and an index holding a timestamp the store does not is
|
|
67
|
+
* drift the startup reconcile would have to clean up every boot.
|
|
68
|
+
*/
|
|
69
|
+
saveReturning(session: Session): Promise<Session>;
|
|
70
|
+
/**
|
|
71
|
+
* Session ids in a state, oldest first — the index a restarting daemon
|
|
72
|
+
* sweeps for turns parked on a human. The `status` column carries it, so
|
|
73
|
+
* no conversation body is deserialized to answer the question.
|
|
74
|
+
*/
|
|
75
|
+
listIdsByStatus(status: SessionStatus): Promise<string[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Every row's routing and lifecycle columns, no bodies — what the
|
|
78
|
+
* startup reconcile compares this shard against the fleet index with.
|
|
79
|
+
*/
|
|
80
|
+
rows(): SessionIndexRow[];
|
|
81
|
+
close(): void;
|
|
82
|
+
}
|
|
83
|
+
/** One session's routing and lifecycle, as the fleet index holds it. */
|
|
84
|
+
export interface SessionIndexRow {
|
|
85
|
+
id: string;
|
|
86
|
+
agentId: string;
|
|
87
|
+
status: SessionStatus;
|
|
88
|
+
createdAt: string;
|
|
89
|
+
updatedAt: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Thrown when a session id another agent already holds is claimed.
|
|
93
|
+
*
|
|
94
|
+
* Sessions never cross agent identities — the dispatcher says so when it
|
|
95
|
+
* finds the conversation, and this is the same rule at the durable seam,
|
|
96
|
+
* where a shared database's primary key used to enforce it for free.
|
|
97
|
+
*/
|
|
98
|
+
export declare class SessionIdTakenError extends Error {
|
|
99
|
+
constructor(sessionId: string, heldBy: string, claimedBy: string);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The fleet's session index, in `fleet.db` beside the schedules.
|
|
103
|
+
*
|
|
104
|
+
* Its own connection rather than a table on the schedule store, for the
|
|
105
|
+
* reason the schedule store gives for its own: `stratus schedules` opens
|
|
106
|
+
* that file from another process, and WAL is what makes two connections on
|
|
107
|
+
* one file routine.
|
|
108
|
+
*/
|
|
109
|
+
export declare class FleetSessionIndex {
|
|
110
|
+
private readonly db;
|
|
111
|
+
constructor(filePath: string, options?: SqliteSessionStoreOptions);
|
|
112
|
+
/**
|
|
113
|
+
* Claim an id for an agent, or refuse it.
|
|
114
|
+
*
|
|
115
|
+
* The claim is the create's single transactional authority: it lands
|
|
116
|
+
* before the shard write, so a crash between the two leaves a claim with
|
|
117
|
+
* no conversation — which the reconcile releases — rather than a
|
|
118
|
+
* conversation no lookup can reach. Re-claiming an id this agent already
|
|
119
|
+
* holds is genuinely a no-op: `create` on an existing id is how a caller
|
|
120
|
+
* re-opens its own session, and `save` claims every id it writes.
|
|
121
|
+
*
|
|
122
|
+
* Taking the id is all this does. It deliberately does NOT write the row
|
|
123
|
+
* it was handed, because the id it is claiming may already have a
|
|
124
|
+
* conversation: overwriting that row's status and timestamps here would
|
|
125
|
+
* announce a transition the shard write has not made yet, and a write
|
|
126
|
+
* that then fails leaves the live daemon answering from a lifecycle row
|
|
127
|
+
* no store agrees with — a pending session missing from recovery until
|
|
128
|
+
* the next restart reconciles it. The row is written by the caller,
|
|
129
|
+
* after the shard write, from what the store actually stored.
|
|
130
|
+
*/
|
|
131
|
+
claim(row: SessionIndexRow): void;
|
|
132
|
+
/** Write the row as it now stands. Called after every shard write. */
|
|
133
|
+
record(row: SessionIndexRow): void;
|
|
134
|
+
/** Drop a claim — a reconcile releasing an id whose conversation never landed. */
|
|
135
|
+
release(id: string): void;
|
|
136
|
+
agentFor(id: string): string | undefined;
|
|
137
|
+
rows(): SessionIndexRow[];
|
|
138
|
+
idsByStatus(status: SessionStatus): string[];
|
|
139
|
+
countByStatus(): Record<string, number>;
|
|
140
|
+
lastActivityByAgent(): Record<string, {
|
|
141
|
+
lastActiveAt: string;
|
|
142
|
+
activeSessions: number;
|
|
143
|
+
}>;
|
|
144
|
+
list(agentId?: string, limit?: number): Array<Pick<Session, 'id' | 'status' | 'createdAt' | 'updatedAt'> & {
|
|
145
|
+
agentId: string;
|
|
146
|
+
}>;
|
|
147
|
+
close(): void;
|
|
148
|
+
}
|
|
149
|
+
/** What a startup reconcile put back in step, for the daemon's log. */
|
|
150
|
+
export interface SessionReconcileReport {
|
|
151
|
+
/** Claims released: an id in the index whose conversation never landed. */
|
|
152
|
+
released: string[];
|
|
153
|
+
/** Sessions re-indexed: a conversation the index had lost, or disagreed about. */
|
|
154
|
+
reindexed: string[];
|
|
155
|
+
}
|
|
156
|
+
export interface ShardedSessionStoreOptions {
|
|
157
|
+
/** Where `agents/` and `fleet.db` live. The default is `~/.stratus`. */
|
|
158
|
+
stateDir: string;
|
|
159
|
+
/**
|
|
160
|
+
* Whether that directory is dedicated Stratus state and may be tightened
|
|
161
|
+
* to owner-only — see {@link SqliteSessionStoreOptions}.
|
|
162
|
+
*/
|
|
163
|
+
ownedDirectory?: boolean;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The fleet's session store: one SQLite database per agent, behind the
|
|
167
|
+
* `SessionStore` every runner already takes, with the fleet-wide reads the
|
|
168
|
+
* control API and the restart sweeps need answered across all of them.
|
|
169
|
+
*
|
|
170
|
+
* A runner is handed *this* — and every write goes to the store opened on
|
|
171
|
+
* its own agent's path. What the facade adds is the set of questions that
|
|
172
|
+
* were never about one agent: which agent holds an id, how many sessions
|
|
173
|
+
* are in each state, when each agent last did anything, and which ids are
|
|
174
|
+
* parked or abandoned fleet-wide.
|
|
175
|
+
*/
|
|
176
|
+
export declare class ShardedSessionStore implements SessionStore {
|
|
177
|
+
private readonly stateDir;
|
|
178
|
+
private readonly ownedDirectory;
|
|
179
|
+
private readonly index;
|
|
180
|
+
private readonly shards;
|
|
181
|
+
constructor(options: ShardedSessionStoreOptions);
|
|
182
|
+
/**
|
|
183
|
+
* One agent's store, opened on first use and kept open.
|
|
184
|
+
*
|
|
185
|
+
* Opened lazily because a roster's agents are not all busy: a daemon
|
|
186
|
+
* with forty souls should hold descriptors for the ones that have
|
|
187
|
+
* conversations, not for every id on disk.
|
|
188
|
+
*/
|
|
189
|
+
private shardFor;
|
|
190
|
+
create(input: Omit<Session, 'createdAt' | 'updatedAt'>): Promise<Session>;
|
|
191
|
+
get(id: string): Promise<Session | undefined>;
|
|
192
|
+
save(session: Session): Promise<void>;
|
|
193
|
+
listIdsByStatus(status: SessionStatus): Promise<string[]>;
|
|
194
|
+
countByStatus(): Record<string, number>;
|
|
195
|
+
lastActivityByAgent(): Record<string, {
|
|
196
|
+
lastActiveAt: string;
|
|
197
|
+
activeSessions: number;
|
|
198
|
+
}>;
|
|
199
|
+
list(agentId?: string, limit?: number): Array<Pick<Session, 'id' | 'status' | 'createdAt' | 'updatedAt'> & {
|
|
200
|
+
agentId: string;
|
|
201
|
+
}>;
|
|
202
|
+
/**
|
|
203
|
+
* Put the index and the shards back in step, before anything reads
|
|
204
|
+
* either. Two durable writes stand where one atomic primary key used to,
|
|
205
|
+
* and this is what makes a crash between them resolve one way rather
|
|
206
|
+
* than leaving them to disagree:
|
|
207
|
+
*
|
|
208
|
+
* - a claim whose conversation never landed is released, freeing the id;
|
|
209
|
+
* - a conversation the index has lost — or disagrees with — is
|
|
210
|
+
* re-indexed from the shard, which is the record;
|
|
211
|
+
* - the same id in two agents' shards is refused loudly, naming both,
|
|
212
|
+
* because picking one would silently hand somebody else's conversation
|
|
213
|
+
* to whoever asks for that id next.
|
|
214
|
+
*/
|
|
215
|
+
reconcile(): Promise<SessionReconcileReport>;
|
|
216
|
+
/**
|
|
217
|
+
* Which agents have a store on disk — the roster is not the answer, on
|
|
218
|
+
* purpose: the gateway keeps a missing soul's sessions when it drops the
|
|
219
|
+
* soul, and a recovery sweep that skipped those would strand exactly the
|
|
220
|
+
* conversations nobody is watching.
|
|
221
|
+
*/
|
|
222
|
+
private shardedAgentIds;
|
|
223
|
+
close(): void;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
|