-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(kap-server): add page mode, updated_before, and batch archive/restore to v2 sessions #2983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
d26ba00
bd51811
2855ee5
6ae1b2f
5614588
fb0a45d
10bd0cd
0273106
d860352
34b0ebf
88215ed
191204c
19c63c5
53a9e3f
0db9570
63a8130
46db11d
62f2e93
7c7d896
c28156a
6d0ea97
e825bc2
fa04d35
568e47d
aa05630
46d88de
2c0e315
165fdf3
b0c43b6
b7a19b6
f18d154
b079259
be79e0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/agent-core-v2": patch | ||
| --- | ||
|
|
||
| Add a cold-session archive/restore path that patches the persisted metadata document, mirrors the flipped summary into the session-index read model, and republishes the archived bus event without materializing the session, backing the new `POST /api/v2/sessions:archive` / `:restore` batch endpoints (per-item results; live sessions still run the full lifecycle). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kap-server": patch | ||
| --- | ||
|
|
||
| Add an `id,archived` item projection to `GET /api/v2/sessions` (`fields=id,archived`): each item trims to `{ id, archived }` for select-all-matching flows, and only that projection gets the relaxed `page_size` ceiling (10000). The projection binds into the page-token fingerprint, rejects unknown fields and `include=git` with `40001`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,14 @@ | ||
| /** | ||
| * `sessionManager` domain — the App-scope session lifecycle facade. | ||
| * | ||
| * Owns the global live-session registry across per-workspace controllers and | ||
| * routes create / resume / restore / close / archive / delete / fork / | ||
| * createChild to the owning controller; per-session lifecycle transitions | ||
| * (and the batch archive critical section) queue on one serialization chain | ||
| * per session. Cold id→workspace lookups go through `sessionIndex`; | ||
| * workspace materialization through `workspaces`. App scope. | ||
| */ | ||
|
|
||
| import { DisposableStore } from '#/_base/di/lifecycle'; | ||
| import { Emitter, type Event, type IWaitUntil } from '#/_base/event'; | ||
| import { ScopeActivation, registerScopedService, type ISessionScopeHandle } from '#/_base/di/scope'; | ||
|
|
@@ -31,6 +42,8 @@ export class SessionManager implements ISessionManager { | |
| declare readonly _serviceBrand: undefined; | ||
| private readonly sessions = new Map<string, ISessionScopeHandle>(); | ||
| private readonly owners = new Map<string, SessionLifecycleService>(); | ||
| private readonly pendingResumes = new Map<string, Promise<ISessionScopeHandle | undefined>>(); | ||
| private readonly lifecycleChains = new Map<string, Promise<void>>(); | ||
| private readonly controllers = new Map<string, SessionControllerEntry>(); | ||
| private readonly controllerEntries = new Set<SessionControllerEntry>(); | ||
| private readonly willCreateEmitter = new Emitter<SessionWillCreateEvent>(); | ||
|
|
@@ -61,35 +74,68 @@ export class SessionManager implements ISessionManager { | |
| } | ||
|
|
||
| async resume(sessionId: string, options?: ResumeSessionOptions): Promise<ISessionScopeHandle | undefined> { | ||
| return (await this.controllerForSession(sessionId))?.resume(sessionId, options); | ||
| const inflight = this.pendingResumes.get(sessionId); | ||
| if (inflight !== undefined) return inflight; | ||
| const promise = this.serializeLifecycle(sessionId, async () => | ||
| (await this.controllerForSession(sessionId))?.resume(sessionId, options), | ||
| ).finally(() => this.pendingResumes.delete(sessionId)); | ||
| this.pendingResumes.set(sessionId, promise); | ||
| return promise; | ||
| } | ||
|
|
||
| get(sessionId: string): ISessionScopeHandle | undefined { | ||
| return this.sessions.get(sessionId); | ||
| } | ||
|
|
||
| async whenResumeSettled(sessionId: string): Promise<void> { | ||
| await this.pendingResumes.get(sessionId)?.catch(() => undefined); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a concurrent resume fails after AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L7-L7 Useful? React with 👍 / 👎. |
||
| await this.owners.get(sessionId)?.whenResumeSettled(sessionId); | ||
| } | ||
|
|
||
| private serializeLifecycle<T>(sessionId: string, work: () => Promise<T>): Promise<T> { | ||
|
liruifengv marked this conversation as resolved.
|
||
| const prev = this.lifecycleChains.get(sessionId) ?? Promise.resolve(); | ||
| const run = prev.then(work, work); | ||
| const next = run.then( | ||
| () => undefined, | ||
| () => undefined, | ||
| ); | ||
| this.lifecycleChains.set(sessionId, next); | ||
| void next.finally(() => { | ||
| if (this.lifecycleChains.get(sessionId) === next) this.lifecycleChains.delete(sessionId); | ||
| }); | ||
| return run; | ||
| } | ||
|
|
||
| withLifecycleSerialization<T>(sessionId: string, work: () => Promise<T>): Promise<T> { | ||
| return this.serializeLifecycle(sessionId, work); | ||
| } | ||
|
|
||
| list(): readonly ISessionScopeHandle[] { | ||
| return [...this.sessions.values()]; | ||
| } | ||
|
|
||
| async close(sessionId: string): Promise<void> { | ||
| await this.owners.get(sessionId)?.close(sessionId); | ||
| await this.serializeLifecycle(sessionId, async () => this.owners.get(sessionId)?.close(sessionId)); | ||
| } | ||
|
|
||
| async archive(sessionId: string): Promise<void> { | ||
| await (await this.controllerForSession(sessionId))?.archive(sessionId); | ||
| } | ||
|
|
||
| async restore(sessionId: string, options?: ResumeSessionOptions): Promise<ISessionScopeHandle | undefined> { | ||
| return (await this.controllerForSession(sessionId))?.restore(sessionId, options); | ||
| return this.serializeLifecycle(sessionId, async () => | ||
| (await this.controllerForSession(sessionId))?.restore(sessionId, options), | ||
| ); | ||
| } | ||
|
|
||
| async delete(sessionId: string): Promise<void> { | ||
| const controller = await this.controllerForSession(sessionId); | ||
| if (controller === undefined) { | ||
| throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`); | ||
| } | ||
| await controller.delete(sessionId); | ||
| await this.serializeLifecycle(sessionId, async () => { | ||
| const controller = await this.controllerForSession(sessionId); | ||
| if (controller === undefined) { | ||
| throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`); | ||
| } | ||
| await controller.delete(sessionId); | ||
| }); | ||
| } | ||
|
|
||
| async fork(options: ForkSessionOptions): Promise<ISessionScopeHandle> { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.