dsh-pause-session 0.0.0-stage → 0.1.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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The controls' phase → view mapping.
3
+ *
4
+ * Kept pure functions of the published reading so the rendering decision — what
5
+ * the button says, whether it accepts a press, and which state label is visible
6
+ * — is asserted without a browser renderer. Labels are KEYS, not copy: the
7
+ * visible sentence is localized, and the host's own names join it as data.
8
+ * @module dsh-pause-session/client/view
9
+ */
10
+ import type { GlobalReading, TriageReading } from './global.js';
11
+ import type { PauseReading } from './control.js';
12
+ import type { PauseKey } from './locales.js';
13
+ import type { TriageAction } from '../shared.js';
14
+ /** What one reading renders as. */
15
+ export interface PauseView {
16
+ /** Whether the host reported this session paused. */
17
+ readonly paused: boolean;
18
+ /** Whether the control accepts a press; an unknown or failed reading does not. */
19
+ readonly enabled: boolean;
20
+ /** Accessible name of the button. */
21
+ readonly ariaKey: PauseKey;
22
+ /** Label shown beside the button while the control has something to say. */
23
+ readonly labelKey?: PauseKey;
24
+ /** Tools in flight, taken verbatim from the host; rendered only with `labelKey`. */
25
+ readonly runningTools: readonly string[];
26
+ /**
27
+ * The host-reported goal opt-out, or `undefined` when the host did not answer
28
+ * it. Absent means the goal switch is not rendered at all: a switch has to
29
+ * state a fact, and the only fact available is the one the record holds.
30
+ */
31
+ readonly rearmGoal?: boolean;
32
+ }
33
+ /**
34
+ * Whether the pause control renders on one session at all.
35
+ *
36
+ * The control is the user's button for a session's work, and that work includes
37
+ * the subagents under it — which is why the press covers the tree and why the
38
+ * button does not belong inside the tree. The client can tell: a Session
39
+ * snapshot carries a `subagent` field, the same signal the read-only composer
40
+ * reads. A subagent's service is not the user's to pause: its parent's control
41
+ * already speaks for it, and a second button here would offer to stop "a
42
+ * session" from inside a child.
43
+ * @param subagent - the session snapshot's own subagent fact, absent when unknown.
44
+ * @returns whether this session's header renders the control.
45
+ */
46
+ export declare function showsPauseControl(subagent: {
47
+ readonly address: unknown;
48
+ } | null | undefined): boolean;
49
+ /**
50
+ * Map one reading to its view.
51
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
52
+ * @returns the view for that reading.
53
+ */
54
+ export declare function pauseView(reading: PauseReading | undefined): PauseView;
55
+ /** One named fact from the whole-app report, with the copy key that introduces it. */
56
+ export interface GlobalCaveat {
57
+ /** What this list of names is. */
58
+ readonly key: PauseKey;
59
+ /** Host names, rendered verbatim. */
60
+ readonly names: readonly string[];
61
+ }
62
+ /** Which verb a whole-app press runs. */
63
+ export type GlobalAction = 'pause-all' | 'resume-all' | 'none';
64
+ /** One decision as the list renders it. */
65
+ export interface TriageActionView {
66
+ /** The decision this button takes. */
67
+ readonly action: TriageAction;
68
+ /** Visible label. */
69
+ readonly labelKey: PauseKey;
70
+ /** What it does, for the accessible name. */
71
+ readonly ariaKey: PauseKey;
72
+ }
73
+ /** The four decisions, in the order a user reads them: least to most cautious. */
74
+ export declare const TRIAGE_ACTION_VIEWS: readonly TriageActionView[];
75
+ /**
76
+ * The fifth listed verb, rendered apart from the four decisions: it is a
77
+ * switch on what the NEXT resume may spend, not a way to decide this row.
78
+ */
79
+ export declare const TRIAGE_GOAL_VIEW: TriageActionView;
80
+ /** One listed session, as the panel renders it. */
81
+ export interface TriageRowView {
82
+ /** Host data, rendered verbatim. */
83
+ readonly sessionId: string;
84
+ /** The durable outcome, as a word. */
85
+ readonly phaseKey: PauseKey;
86
+ /** Stop position: the event the log ends on and the step it belongs to. */
87
+ readonly stopped?: string;
88
+ /** The tools whose outcomes were never recorded. */
89
+ readonly unknownTools: readonly string[];
90
+ /** Whether the host said it can hand this session the request it owes. */
91
+ readonly wakeable: boolean;
92
+ /** Whether the log could be read at all. */
93
+ readonly readable: boolean;
94
+ /** Whether resuming this session may re-open its goal and spend its rounds. */
95
+ readonly rearmGoal: boolean;
96
+ }
97
+ /** What the decision-list panel renders. */
98
+ export interface TriageView {
99
+ /** Whether the panel has anything to show; a host that cannot answer shows nothing. */
100
+ readonly visible: boolean;
101
+ /** Whether any listed session can actually be continued. */
102
+ readonly rows: readonly TriageRowView[];
103
+ }
104
+ /**
105
+ * Map one decision-list reading to its view.
106
+ * @param reading - the host-reported list, or `undefined` before the panel asked.
107
+ * @returns the rows as the panel renders them.
108
+ */
109
+ export declare function triageView(reading: TriageReading | undefined): TriageView;
110
+ /** What the whole-app reading renders as. */
111
+ export interface GlobalPauseView {
112
+ /** Whether the control renders at all; a control with no host answer claims nothing. */
113
+ readonly visible: boolean;
114
+ /** Whether the control accepts a press. */
115
+ readonly enabled: boolean;
116
+ /** Accessible name of the button. */
117
+ readonly ariaKey: PauseKey;
118
+ /**
119
+ * Headline shown beside the button. Absent while there is nothing to report:
120
+ * an untouched park renders the bare affordance, not a sentence about itself.
121
+ */
122
+ readonly labelKey?: PauseKey;
123
+ /** Verb the press runs. */
124
+ readonly action: GlobalAction;
125
+ /** Host numbers, rendered verbatim beside the label. */
126
+ readonly detail: string;
127
+ /** How many decisions are waiting; shown on the panel's own entry point. */
128
+ readonly unclean: number;
129
+ /** Named facts the user must not miss; rendered on the tooltip. */
130
+ readonly caveats: readonly GlobalCaveat[];
131
+ }
132
+ /**
133
+ * Map one whole-app reading to its view.
134
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
135
+ * @returns the view for that reading.
136
+ */
137
+ export declare function globalPauseView(reading: GlobalReading | undefined): GlobalPauseView;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Reading one parked session's stored log through a bound, and the facts the
3
+ * morning list may claim from what it read.
4
+ *
5
+ * The decision list reads the same log the wake reads, but for the opposite
6
+ * reason: the wake has to reproduce the session, so it reads everything. The
7
+ * list only has to name where the session stopped, which tool call the log
8
+ * never heard back from, and whether an answer is still owed — all of them
9
+ * facts about the log's END. Reading from seq 0 to the end for every row, on
10
+ * every command-registry change while the panel is open, would make a long
11
+ * session's cost grow with a log nobody is asking about.
12
+ *
13
+ * WHAT THE BOUND BUYS. A stored log is append-only, so the tail is stable: the
14
+ * read takes the last {@link TRIAGE_WINDOW_EVENTS} events, or the whole log when
15
+ * it is shorter, and the answer is derived from that. The window is complete
16
+ * whenever the log fits inside it, which is the common case; the fold the rest
17
+ * of this plugin reads history through is then the harness's own, over exactly
18
+ * the events the harness would fold.
19
+ *
20
+ * WHAT THE TAIL ALONE PROVES. A truncated window still answers the list's
21
+ * questions, for one reason the harness guarantees: a dangling tool call exists
22
+ * only inside the step that is still open, because a step's end is written
23
+ * after its results. So an unanswered call is always inside the final open
24
+ * span, and the tail that contains that span contains every one of them. What
25
+ * the window cannot prove — a log a replacement event rewrote from further back
26
+ * — is not guessed at: {@link logFacts} reports `undefined` and the row says
27
+ * `unreadable`, because a list that invents a stop position is worse than a
28
+ * list that says it could not read one.
29
+ * @module dsh-pause-session/coldlog
30
+ */
31
+ import type { Context } from '@deepseek-ai/cordis';
32
+ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
33
+ import type { TriageTrace } from './shared.js';
34
+ /**
35
+ * Events the read looks at before it gives up. Sized to hold a whole final
36
+ * step — the model exchange, its tool calls, and their results — with room to
37
+ * spare, at a cost that does not grow with the session.
38
+ */
39
+ export declare const TRIAGE_WINDOW_EVENTS = 2000;
40
+ /**
41
+ * The longest log the read will walk while locating where its tail starts.
42
+ *
43
+ * The window's start is found by probing for the log's length, so a log longer
44
+ * than this cannot be located from its end at a cost worth paying for a panel
45
+ * left open. Such a log — a million stored events — is reported unreadable
46
+ * rather than read from the front, which is the read this module exists to
47
+ * avoid.
48
+ */
49
+ export declare const TRIAGE_LOG_MAX_EVENTS = 1000000;
50
+ /** How one stored log was seen by the bounded read. */
51
+ export type ColdLog =
52
+ /** The events the read returned, and whether they are the whole stored log. */
53
+ {
54
+ readonly kind: 'read';
55
+ readonly events: readonly SessionEvent[];
56
+ readonly complete: boolean;
57
+ }
58
+ /** The log could not be opened or read, or it is longer than this read will walk. */
59
+ | {
60
+ readonly kind: 'unreadable';
61
+ };
62
+ /** The facts the decision list claims, all of them read off the log's end. */
63
+ export interface ColdFacts {
64
+ /** What the stored log's last event was, and the turn/step it belongs to. */
65
+ readonly last: SessionEvent | undefined;
66
+ /** Tool calls the stored events record and no stored result answers. */
67
+ readonly traces: readonly TriageTrace[];
68
+ /** Whether the stored log ends owing the model an answer. */
69
+ readonly owed: boolean;
70
+ /** Whether the log names the route a wake would be addressed to. */
71
+ readonly wakeable: boolean;
72
+ }
73
+ /**
74
+ * The turn and step an event belongs to, when it carries them.
75
+ * @param event - one stored event.
76
+ * @returns the position fields the event has, and nothing for the fields it does not.
77
+ */
78
+ export declare function positionOf(event: SessionEvent): {
79
+ readonly turn?: number;
80
+ readonly step?: number;
81
+ };
82
+ /**
83
+ * The facts one bounded read of a stored log supports.
84
+ *
85
+ * A complete read is folded by the harness's own derivation, so the list and
86
+ * the wake answer the "does it still owe a reply" question through one rule.
87
+ * A truncated read answers from the tail, and only where the tail is proof: a
88
+ * replacement event that rewrote a surface range from outside the window is
89
+ * not visible here, so a log whose tail a later event could have rewritten is
90
+ * reported unreadable rather than guessed at.
91
+ * @param events - the stored events the read returned, in sequence order.
92
+ * @param complete - whether those events are the whole stored log.
93
+ * @param projections - this process's registered message projections.
94
+ * @returns the row's facts, or `undefined` when this read cannot support them.
95
+ */
96
+ export declare function logFacts(events: readonly SessionEvent[], complete: boolean, projections: Context['sessions']['messageProjections']): ColdFacts | undefined;
97
+ /**
98
+ * Read one session's stored log, bounded to the events the list's facts need.
99
+ *
100
+ * The window is the last `windowEvents` events, found by probing for the log's
101
+ * length and then reading exactly that slice. A short log is returned whole and
102
+ * marked complete; a log longer than {@link TRIAGE_LOG_MAX_EVENTS} is reported
103
+ * unreadable without being read at all.
104
+ * @param ctx - host context carrying the optional persistence service.
105
+ * @param sessionId - the stored session to read.
106
+ * @param windowEvents - how many events of the log's end one row may fold.
107
+ * @param signal - cancellation for the open, probe, and read.
108
+ * @returns the bounded read, or `unreadable` when no read seam exists, it refused, or the log is too long to walk.
109
+ * @throws the cancellation reason when `signal` aborts the read.
110
+ */
111
+ export declare function coldRead(ctx: Context, sessionId: SessionId, windowEvents?: number, signal?: AbortSignal): Promise<ColdLog>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Rebuild the scoped Agent composition a parked Session ran under.
3
+ *
4
+ * Activation is not only a persisted Session: the Agent preset supplies the
5
+ * scoped tools, prompt sections, skills, and — for a subagent child — the
6
+ * delegation context, persona, and tool restriction that were in force before
7
+ * the park. A resume that loads the log without that setup starts a different
8
+ * Agent on the same history.
9
+ *
10
+ * The preset a Session runs is the `agentPreset` projection, never the
11
+ * creation-time header alone: a blank Session may select another preset before
12
+ * its first turn, and the projection records the selection that actually ran.
13
+ * A child with no independent selection joins its live parent's preset
14
+ * generation; one that selected independently mounts the projected preset.
15
+ * The durable child descriptor supplies the persona, tool filter, and route
16
+ * that are reapplied only on cold resume.
17
+ *
18
+ * Both services are optional. A profile without `agentPresets` has no scoped
19
+ * preset composition to restore, and this module leaves the resume unchanged.
20
+ * A profile that mounts one but cannot read the Session projection cannot
21
+ * prove what composition the Session ran, so it fails the resume instead of
22
+ * silently starting a different Agent.
23
+ * @module dsh-pause-session/composition
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
26
+ import type { Agent, AgentOptions, AgentSetup } from '@deepseek-ai/dsh-agent';
27
+ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
28
+ /** The setup and route options a resumed Agent receives from its stored composition. */
29
+ export interface RestoredComposition {
30
+ /** The durable child route, when the descriptor recorded one. */
31
+ readonly agentOptions?: AgentOptions;
32
+ /** The scoped Agent world to mount before the Agent is published. */
33
+ readonly setup?: AgentSetup;
34
+ }
35
+ /**
36
+ * A stored subagent route that no resume may reactivate.
37
+ *
38
+ * A one-shot child is terminal by contract; activating it as a root Agent
39
+ * would turn a finished delegation into an ordinary conversation that happens
40
+ * to share its Session id.
41
+ */
42
+ export declare class UnresumableSessionError extends Error {
43
+ constructor(sessionId: SessionId);
44
+ }
45
+ /**
46
+ * Whether the child's own log selected a preset before its descriptor.
47
+ *
48
+ * The creation-time selection precedes the first `subagent/descriptor`; a
49
+ * selection after the descriptor belongs to the child's own later history and
50
+ * does not decide which composition cold resume joins. This mirrors the
51
+ * subagent runtime's own cold-resume rule.
52
+ * @param events - the child's own log suffix, without the fork-inherited prefix.
53
+ * @returns whether the child carries an independent preset selection.
54
+ */
55
+ export declare function hasIndependentPresetSelection(events: readonly SessionEvent[]): boolean;
56
+ /**
57
+ * Whether a local Session may be parked under a promise of a later resume.
58
+ *
59
+ * A normal root Session has no child descriptor and is resumable. A local
60
+ * subagent child is resumable only when its own durable descriptor is
61
+ * `continuable`; a historical `one-shot` child is terminal and must never be
62
+ * registered as a park the morning path would later have to refuse. External
63
+ * executions have no local Session, so callers never reach this check.
64
+ *
65
+ * A profile without the optional Session-query service cannot prove the
66
+ * descriptor's mode; it keeps the previous permissive behavior rather than
67
+ * dropping every subagent. The web profile mounts the query service.
68
+ * @param ctx - host context carrying the optional Session-query service.
69
+ * @param sessionId - the local Session to inspect.
70
+ * @param signal - cancellation for the observation read.
71
+ * @returns whether a park of this Session may be resumed.
72
+ * @throws the cancellation reason when `signal` aborts the read.
73
+ */
74
+ export declare function isResumableSession(ctx: Context, sessionId: SessionId, signal?: AbortSignal): Promise<boolean>;
75
+ /**
76
+ * Read one parked Session's stored composition and build the resume options.
77
+ *
78
+ * The returned `setup` belongs to the unpublished Agent scope, so all
79
+ * registrations exist before `agent/created`. For a root Session the setup
80
+ * mounts the Session's projected preset, or the profile's default when the
81
+ * Session never selected one. For a child under a live parent it applies the
82
+ * child composition: an independent preset mounts separately, while an
83
+ * inherited composition joins the parent's current generation; the historical
84
+ * descriptor's persona and tool filter are reapplied in both cases.
85
+ * @param ctx - host context carrying the optional preset and Session-query services.
86
+ * @param sessionId - the parked Session being resumed.
87
+ * @param parent - the live direct parent, when a child is resumed under it.
88
+ * @param signal - cancellation for the observation read.
89
+ * @returns the Agent options and setup to pass to `agents.resume`.
90
+ * @throws {UnresumableSessionError} when the stored child route is one-shot.
91
+ * @throws when a mounted preset roster cannot read the Session projection, or
92
+ * a recorded preset cannot be restored.
93
+ */
94
+ export declare function restoreComposition(ctx: Context, sessionId: SessionId, parent: Agent | undefined, signal?: AbortSignal): Promise<RestoredComposition>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The session control the browser half drives: the pause verb, the park state
3
+ * machine behind it, and the live state channel that accompanies both.
4
+ *
5
+ * A third-party plugin can add neither a `ctx.remote` namespace nor a forwarded
6
+ * event: the Client's Remote namespaces and the forwarded-event allowlist are
7
+ * compiled into the harness's own API assembly. The documented Client-callable
8
+ * Host entry point is therefore a session command run through
9
+ * `ctx.remote.commands.execute`, and the one live channel such a plugin can
10
+ * publish through is the command registry itself — `commands/change` is a
11
+ * forwarded event, and `commands.list(sessionId)` is a Client-callable read.
12
+ *
13
+ * That same registry is how a phase change leaves this process: a transition
14
+ * re-registers the status verb, which emits the change a client needs to read
15
+ * the new phase. It is the only way a third-party plugin can announce anything,
16
+ * and it is what makes the flush that resolves `parking` into `safe` observable
17
+ * at all — that step changes no registration of its own.
18
+ *
19
+ * So the resume definition IS the paused marker: it is registered on a paused
20
+ * session's own agent context and removed on resume, and the browser half reads
21
+ * "paused" from that registration rather than inventing it. A descriptor cannot
22
+ * change after registration, so the park state rides a companion status
23
+ * definition (registered beside the marker) whose answer is derived per call
24
+ * rather than frozen into a descriptor — see `./shared.ts`. That one read
25
+ * carries the record's goal opt-out too, so the session-header control can show
26
+ * and switch it while the session is parked and clean — the state in which a
27
+ * silent morning resume would otherwise spend the user's rounds unasked. The
28
+ * switch is addressed to the session it changes, so it needs no listed row.
29
+ *
30
+ * The header control is the user's button for ONE session's work, and that work
31
+ * includes the subagents under it: the press registers the session and its
32
+ * descendant tree, the release takes the same tree back, and the status the
33
+ * header shows is the worst phase among the members the press registered. The
34
+ * whole-app control covers every running tree and answers for all of them; this
35
+ * one answers for the tree it owns.
36
+ *
37
+ * A mark is not a park. A session is parked only once nothing is left to drain:
38
+ * no call in flight AND either its next model request is held at the gate or no
39
+ * turn is open at all (there will be no next request to hold). Only then is the
40
+ * session flushed, at the session level — `SessionHandle.flush()` does not
41
+ * drain the routed live buffer, so it would record a park that a crash erases.
42
+ * The "safe to close" phase appears only after that flush resolves.
43
+ *
44
+ * Every phase a pause reaches is written to the durable registry before it is
45
+ * published — including the move into `parking`, which the status command may
46
+ * therefore never serve ahead of the record. A restart re-holds exactly the
47
+ * sessions the last process left parked, and a phase that could not be recorded
48
+ * is never shown as cleaner than the record says.
49
+ *
50
+ * A park verb changes state, so it acts only on a person's command: a command
51
+ * dispatched from inside a model turn is refused. The listed verbs add the
52
+ * one binding their target needs — the row must be one the calling session was
53
+ * shown — and that rule lives with the list (`./triage.ts`).
54
+ * @module dsh-pause-session/control
55
+ */
56
+ import type { Context } from '@deepseek-ai/cordis';
57
+ import { SessionId } from '@deepseek-ai/dsh-session';
58
+ import type { PauseGate } from './gate.js';
59
+ import type { ParkWatch } from './park.js';
60
+ import type { ParkRegistry } from './registry.js';
61
+ import type { ResumeAttempt, ResumeOptions } from './resume.js';
62
+ /**
63
+ * Result copy for a state-changing park verb issued from inside a model turn.
64
+ * The invocation carries no origin of its own, so this refusal is the plugin's
65
+ * own reading of the one signal that separates a person's command from a
66
+ * model's.
67
+ */
68
+ export declare const UNATTENDED_TEXT: string;
69
+ /** What one tree-wide press registered, and what it could not. */
70
+ export interface PressTreeOutcome {
71
+ /** How many sessions this press covered: the session plus its descendants. */
72
+ readonly members: number;
73
+ /** How many of them now have a durable park record behind them. */
74
+ readonly registered: number;
75
+ /** Whether the pressed session itself is one of them. */
76
+ readonly rootRegistered: boolean;
77
+ /** The members whose record could not be written; they are not paused. */
78
+ readonly unrecorded: readonly SessionId[];
79
+ /** Whether the tree walk itself failed, so the session's subagents were not covered. */
80
+ readonly unread: boolean;
81
+ }
82
+ /** What mounting the session control hands back to the plugin entry. */
83
+ export interface SessionControl {
84
+ /**
85
+ * Hold every session the registry remembers and can read, and publish the
86
+ * marker for the ones that already have a live agent. Called once, right
87
+ * after mount.
88
+ */
89
+ adopt(): Promise<void>;
90
+ }
91
+ /** How resuming all parked sessions answers the command that asked for it. */
92
+ export declare function summarizeResume(attempts: readonly ResumeAttempt[]): string;
93
+ /**
94
+ * Everything the session control needs from the composition: the clean resume
95
+ * path's route knowledge, plus the bound the decision list reads each parked
96
+ * log under.
97
+ */
98
+ export interface SessionControlOptions extends ResumeOptions {
99
+ /** Events of each stored log the decision list may fold per row; see `./coldlog.ts`. */
100
+ readonly maxListedLogEvents?: number;
101
+ }
102
+ /**
103
+ * Contribute the session control: the pause verb, the per-session status read,
104
+ * the global resume verb, and — for every paused session that has a live agent
105
+ * — the resume marker.
106
+ * @param ctx - host context carrying the command registry, the agent store, the
107
+ * session store, and the projection registry.
108
+ * @param gate - the pause gate this control drives and reflects.
109
+ * @param watch - the per-session activity fold the park predicate reads.
110
+ * @param registry - the durable pause list every phase change is written to.
111
+ * @param options - route knowledge the clean resume path needs, and the decision list's read bound.
112
+ * @returns the mount-time adoption step and the morning resume path.
113
+ */
114
+ export declare function installSessionControl(ctx: Context, gate: PauseGate, watch: ParkWatch, registry: ParkRegistry, options: SessionControlOptions): SessionControl;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The pause gate: one session's hold at the model-request waterfall.
3
+ *
4
+ * The hold sits on the `llm/stream` waterfall, which is the only point that
5
+ * satisfies all four conditions at once: the previous step's tool calls have
6
+ * drained, the request is already built, the session's pending input is
7
+ * committed, and — because nothing pulls the adapter — no timer is armed
8
+ * anywhere. Machine sleep, network loss, and the adapter's stream-idle
9
+ * watchdog therefore cannot touch a parked request.
10
+ * @module dsh-pause-session/gate
11
+ */
12
+ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
13
+ import type { SessionId } from '@deepseek-ai/dsh-session';
14
+ /** Message of the failure a held request ends with when its plugin unloads first. */
15
+ export declare const PLUGIN_UNLOADED = "pause: plugin lifetime ended while a request was held";
16
+ /** Options of {@link PauseGate}. */
17
+ export interface PauseGateOptions {
18
+ /** Aborted when the owning plugin unloads; a held request must never delegate after that. */
19
+ readonly lifetime: AbortSignal;
20
+ }
21
+ /** Per-session pause state and the waterfall listener that enforces it. */
22
+ export declare class PauseGate {
23
+ private readonly options;
24
+ /** Paused sessions; membership IS the pause state. */
25
+ private readonly paused;
26
+ private readonly listeners;
27
+ private readonly heldListeners;
28
+ /**
29
+ * @param options - the plugin-lifetime signal every held request races.
30
+ */
31
+ constructor(options: PauseGateOptions);
32
+ /**
33
+ * Subscribe to pause-state changes.
34
+ * @param listener - called with the session whose state changed and its new state.
35
+ * @returns the unsubscribe function.
36
+ */
37
+ onChange(listener: (sessionId: SessionId, paused: boolean) => void): () => void;
38
+ /**
39
+ * Subscribe to "a request arrived at / left the gate" for a paused session.
40
+ *
41
+ * This is the moment a session can become parked, so it is also the moment a
42
+ * park observer has to look. Listeners run synchronously inside the request
43
+ * path: they must only record that a look is owed.
44
+ * @param listener - called with the session and whether it now holds a request.
45
+ * @returns the unsubscribe function.
46
+ */
47
+ onHeld(listener: (sessionId: SessionId, held: boolean) => void): () => void;
48
+ /**
49
+ * @param sessionId - session to inspect.
50
+ * @returns whether the session is currently paused.
51
+ */
52
+ isPaused(sessionId: SessionId): boolean;
53
+ /**
54
+ * @param sessionId - session to inspect.
55
+ * @returns whether a model request of that session is waiting at the gate
56
+ * right now. False for a session that is paused but whose loop has not built
57
+ * its next request yet — a running tool, for instance, is still draining.
58
+ */
59
+ isHeld(sessionId: SessionId): boolean;
60
+ /**
61
+ * Enter one request that is about to wait. The count rises synchronously with
62
+ * {@link hold}, so an observer notified at that moment reads `isHeld` as true.
63
+ * @param pause - the session's pause record.
64
+ * @returns the exit that lowers the count again.
65
+ */
66
+ private enter;
67
+ /**
68
+ * Pause one session: its next model request is held until {@link resume}. A
69
+ * request already past the waterfall keeps running — pausing never
70
+ * interrupts a model call or a tool call that has started.
71
+ * @param sessionId - session to pause.
72
+ * @returns whether this call changed the state.
73
+ */
74
+ pause(sessionId: SessionId): boolean;
75
+ /**
76
+ * Resume one session: release everything it holds and stop holding what
77
+ * follows. The requests sent from then on are the ones built before the
78
+ * hold, so a resumed session continues from the point it stopped.
79
+ * @param sessionId - session to resume.
80
+ * @returns whether this call changed the state.
81
+ */
82
+ resume(sessionId: SessionId): boolean;
83
+ /**
84
+ * The wait one model request must finish before it may delegate, and the exit
85
+ * that reports the request as no longer waiting.
86
+ * @param request - the request reaching the waterfall.
87
+ * @returns the wait plus its release, or `undefined` when this gate does not
88
+ * hold that request.
89
+ */
90
+ hold(request: GenerateOptions): {
91
+ readonly wait: Promise<void>;
92
+ readonly left: () => void;
93
+ } | undefined;
94
+ /**
95
+ * The `llm/stream` listener.
96
+ *
97
+ * `ctx.waterfall` composes listeners synchronously, so this returns an async
98
+ * iterator synchronously — an `async` listener would hand the loop a promise
99
+ * where it iterates a stream. One generator body reaches `next()` at most
100
+ * once, which is what makes delegation single-shot per HELD request; a flag
101
+ * shared across requests would also short-circuit everything issued after
102
+ * the release.
103
+ * @param request - the full request, already built and frozen by the loop.
104
+ * @param next - the downstream waterfall chain.
105
+ * @returns the stream, delayed until this request may delegate.
106
+ */
107
+ readonly stream: (request: GenerateOptions, next: () => AsyncIterable<StreamChunk>) => AsyncIterable<StreamChunk>;
108
+ private notify;
109
+ private notifyHeld;
110
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The whole-app park: one action that registers every running session tree,
3
+ * and the aggregate answer to "can I close the lid?".
4
+ *
5
+ * The per-session control owns one session's park; this module owns the set.
6
+ * It reads three real sources and never infers what they can say themselves:
7
+ *
8
+ * - `ctx.agents.list()` plus the park activity fold decide which sessions are
9
+ * running. A session with no live agent is not running, and a session with
10
+ * nothing unfinished — no tool in flight, no turn open, no delivery the loop
11
+ * has not claimed — is left alone.
12
+ * - The subagent runtime's descendant rows decide the tree and the one-shot
13
+ * fact. A row's `mode` is the harness's own durable record of whether that
14
+ * child can be resumed after a restart; a branch the walk could not read is
15
+ * reported as unread rather than assumed clean.
16
+ * - The job registry decides which background jobs belong to the registered
17
+ * trees. Those run outside the agent loop: this plugin cannot stop them, so
18
+ * it names them.
19
+ *
20
+ * The aggregate is derived in one place so the host's answer and the browser's
21
+ * reading of it cannot disagree, and it is a promise about the durable
22
+ * registry: a session is `safe` only when its recorded phase says so.
23
+ * @module dsh-pause-session/global
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
26
+ import type { CommandResult } from '@deepseek-ai/dsh-commands';
27
+ import type { SessionId } from '@deepseek-ai/dsh-session';
28
+ import type { ParkWatch } from './park.js';
29
+ import type { ParkRegistry } from './registry.js';
30
+ import type { ParkPhase } from './shared.js';
31
+ /** One session's live park state, as the per-session control sees it. */
32
+ export interface LivePark {
33
+ readonly phase: ParkPhase;
34
+ /** Whether this process can still walk the session to a flushed park. */
35
+ readonly moving: boolean;
36
+ }
37
+ /** What one press of the per-session control did. */
38
+ export type PressResult = 'registered' | 'unrecordable' | 'not-live' | 'not-resumable' | 'already-paused';
39
+ /** What the whole-app control needs from the per-session control. */
40
+ export interface GlobalPauseDeps {
41
+ /**
42
+ * @param sessionId - session to inspect.
43
+ * @returns whether a press already owns this session.
44
+ */
45
+ isPaused(sessionId: SessionId): boolean;
46
+ /**
47
+ * Press one session's pause and make it durable.
48
+ * @param sessionId - session to register.
49
+ * @param signal - cancellation for the resumability and registry reads.
50
+ * @returns what the press did; only `registered` created a new park.
51
+ */
52
+ press(sessionId: SessionId, signal?: AbortSignal): Promise<PressResult>;
53
+ /**
54
+ * Look at one session's mark now that it is paused.
55
+ * @param sessionId - the session that was just pressed.
56
+ */
57
+ look(sessionId: SessionId): void;
58
+ /**
59
+ * @returns every session a live mark owns, with the phase it reports now.
60
+ */
61
+ live(): ReadonlyMap<SessionId, LivePark>;
62
+ /**
63
+ * @returns every registered session this process could not hold again — a
64
+ * record whose session cannot be read here, so nothing in this process can
65
+ * park it, release it, or act on it.
66
+ */
67
+ unheld(): ReadonlySet<SessionId>;
68
+ }
69
+ /** The whole-app park, as the plugin entry drives it. */
70
+ export interface GlobalPause {
71
+ /**
72
+ * Register and park every running session tree in one action. Sessions this
73
+ * pause already owns are left where they are; an idle session is not touched.
74
+ * @param signal - cancellation for the tree reads this action performs.
75
+ * @returns the aggregate as of this action's end.
76
+ */
77
+ pressAll(signal?: AbortSignal): Promise<CommandResult>;
78
+ /**
79
+ * @param signal - cancellation for the tree reads this read performs.
80
+ * @returns the aggregate as of this read.
81
+ */
82
+ readStatus(signal?: AbortSignal): Promise<CommandResult>;
83
+ /**
84
+ * Report one session a press could not register, so the aggregate names it.
85
+ * The whole-app action records that fact for its own targets; a press of a
86
+ * session's tree reports the members the registry refused through here.
87
+ * @param sessionId - the session a press tried and failed to register.
88
+ * @param unrecorded - whether it is now known to be unparked for that reason.
89
+ */
90
+ markUnrecorded(sessionId: SessionId, unrecorded: boolean): void;
91
+ }
92
+ /**
93
+ * Mount the whole-app park.
94
+ * @param ctx - host context carrying the agent store and the optional subagent
95
+ * and job services.
96
+ * @param watch - the activity fold that says whether a session has unfinished work.
97
+ * @param registry - the durable list this aggregate is a promise about.
98
+ * @param deps - the per-session press and the live phase of every mark.
99
+ * @returns the one-press action and the aggregate read.
100
+ */
101
+ export declare function mountGlobalPause(ctx: Context, watch: ParkWatch, registry: ParkRegistry, deps: GlobalPauseDeps): GlobalPause;