dsh-pause-session 0.0.0-stage → 0.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/DEVELOPMENT.md +78 -0
  3. package/LICENSE +21 -0
  4. package/README.md +140 -2
  5. package/README.zh.md +101 -0
  6. package/REHEARSAL.md +90 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/images/pause-controls.webp +0 -0
  9. package/icon.svg +5 -0
  10. package/lib/client.js +2024 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/index.js +2996 -0
  13. package/lib/index.js.map +1 -0
  14. package/lib/typert.host.d.ts +3 -0
  15. package/lib/typert.host.js +339 -0
  16. package/lib/typert.remote-client.d.ts +38 -0
  17. package/lib/typert.remote-client.d.ts.map +1 -0
  18. package/lib/typert.remote-client.js +244 -0
  19. package/lib/types/client/control.d.ts +99 -0
  20. package/lib/types/client/global.d.ts +125 -0
  21. package/lib/types/client/icons.d.ts +24 -0
  22. package/lib/types/client/index.d.ts +78 -0
  23. package/lib/types/client/locales.d.ts +67 -0
  24. package/lib/types/client/pill.d.ts +49 -0
  25. package/lib/types/client/remote.d.ts +71 -0
  26. package/lib/types/client/view.d.ts +151 -0
  27. package/lib/types/coldlog.d.ts +111 -0
  28. package/lib/types/composition.d.ts +94 -0
  29. package/lib/types/control.d.ts +95 -0
  30. package/lib/types/gate.d.ts +110 -0
  31. package/lib/types/global.d.ts +103 -0
  32. package/lib/types/history.d.ts +47 -0
  33. package/lib/types/index.d.ts +64 -0
  34. package/lib/types/park-state.d.ts +51 -0
  35. package/lib/types/park.d.ts +50 -0
  36. package/lib/types/registry.d.ts +117 -0
  37. package/lib/types/release-order.d.ts +21 -0
  38. package/lib/types/remote-adapter.d.ts +22 -0
  39. package/lib/types/remote.d.ts +158 -0
  40. package/lib/types/resume-activation.d.ts +27 -0
  41. package/lib/types/resume-text.d.ts +67 -0
  42. package/lib/types/resume.d.ts +126 -0
  43. package/lib/types/shared.d.ts +221 -0
  44. package/lib/types/snapshot.d.ts +29 -0
  45. package/lib/types/tree.d.ts +71 -0
  46. package/lib/types/triage.d.ts +120 -0
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +166 -3
@@ -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 time the panel opens, 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,95 @@
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
+ * The host exposes this control through the native Typert Remote namespace:
6
+ * `observe` streams snapshots and `act` / `triage` / `decide` drive the UI. No
7
+ * slash command is registered. Phase changes wake the Remote stream through a
8
+ * local listener set; the durable registry remains the source of truth for what
9
+ * the snapshot reports.
10
+ * @module dsh-pause-session/control
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import { SessionId } from '@deepseek-ai/dsh-session';
14
+ import type { PauseGate } from './gate.js';
15
+ import type { ParkWatch } from './park.js';
16
+ import type { ParkRegistry } from './registry.js';
17
+ import type { ReleaseTreeOutcome, ResumeAttempt, ResumeOptions } from './resume.js';
18
+ import type { TriageAction } from './shared.js';
19
+ import type { PauseSnapshot, TriageList } from './shared.js';
20
+ import type { PauseAnswer } from './shared.js';
21
+ /** What one tree-wide press registered, and what it could not. */
22
+ export interface PressTreeOutcome {
23
+ /** How many sessions this press covered: the session plus its descendants. */
24
+ readonly members: number;
25
+ /** How many of them now have a durable park record behind them. */
26
+ readonly registered: number;
27
+ /** Whether the pressed session itself is one of them. */
28
+ readonly rootRegistered: boolean;
29
+ /** The members whose record could not be written; they are not paused. */
30
+ readonly unrecorded: readonly SessionId[];
31
+ /** Whether any branch was unread, so the subagent tree was not fully covered. */
32
+ readonly unread: boolean;
33
+ /** Tree members on a one-shot or unreadable route; they were not registered. */
34
+ readonly unresumable: readonly SessionId[];
35
+ /** Branch ids the tree walk could not read. */
36
+ readonly unreadBranches: readonly SessionId[];
37
+ }
38
+ /** What mounting the session control hands back to the plugin entry. */
39
+ export interface SessionControl {
40
+ /**
41
+ * Hold every session the registry remembers and can read, and publish the
42
+ * marker for the ones that already have a live agent. Called once, right
43
+ * after mount.
44
+ */
45
+ adopt(): Promise<void>;
46
+ /**
47
+ * Read the whole app and every live paused session as one JSON-safe snapshot.
48
+ * The native snapshot serves this; it never enters a Session log.
49
+ * @param signal - cancellation for the tree reads this snapshot performs.
50
+ * @returns the whole-app verdict and the paused sessions this process holds.
51
+ */
52
+ readSnapshot(signal?: AbortSignal): Promise<PauseSnapshot>;
53
+ /** Wake a listener after any visible status change. */
54
+ subscribe(listener: () => void): () => void;
55
+ /** Press one Session tree. */
56
+ pressTree(sessionId: SessionId, signal?: AbortSignal): Promise<PressTreeOutcome>;
57
+ /** Release one Session's registered tree. */
58
+ releaseTree(sessionId: SessionId, signal?: AbortSignal): Promise<ReleaseTreeOutcome>;
59
+ /** Press every running Session tree. */
60
+ pauseAll(signal?: AbortSignal): Promise<PauseAnswer>;
61
+ /** Resume every cleanly paused Session. */
62
+ resumeAll(signal?: AbortSignal): Promise<PauseAnswer>;
63
+ /** Release every registered tree, cancelling a press that is still draining. */
64
+ cancelAll(signal?: AbortSignal): Promise<PauseAnswer>;
65
+ /** Flip one Session's goal re-arm opt-out. */
66
+ toggleGoal(sessionId: SessionId, signal?: AbortSignal): Promise<PauseAnswer>;
67
+ /** Read the morning list for one calling Session. */
68
+ readTriage(caller: SessionId, signal?: AbortSignal): Promise<TriageList>;
69
+ /** Take one listed decision for one calling Session. */
70
+ decideTriage(caller: SessionId, target: SessionId, action: TriageAction, signal?: AbortSignal): Promise<PauseAnswer>;
71
+ }
72
+ /** How resuming all parked sessions answers the caller. */
73
+ export declare function summarizeResume(attempts: readonly ResumeAttempt[]): string;
74
+ /**
75
+ * Everything the session control needs from the composition: the clean resume
76
+ * path's route knowledge, plus the bound the decision list reads each parked
77
+ * log under.
78
+ */
79
+ export interface SessionControlOptions extends ResumeOptions {
80
+ /** Events of each stored log the decision list may fold per row; see `./coldlog.ts`. */
81
+ readonly maxListedLogEvents?: number;
82
+ }
83
+ /**
84
+ * Contribute the session control: the pause verb, the per-session status read,
85
+ * the global resume verb, and — for every paused session that has a live agent
86
+ * — the resume marker.
87
+ * @param ctx - host context carrying the agent store, the
88
+ * session store, and the projection registry.
89
+ * @param gate - the pause gate this control drives and reflects.
90
+ * @param watch - the per-session activity fold the park predicate reads.
91
+ * @param registry - the durable pause list every phase change is written to.
92
+ * @param options - route knowledge the clean resume path needs, and the decision list's read bound.
93
+ * @returns the mount-time adoption step and the morning resume path.
94
+ */
95
+ 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,103 @@
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 { SessionId } from '@deepseek-ai/dsh-session';
27
+ import type { ParkWatch } from './park.js';
28
+ import type { ParkRegistry } from './registry.js';
29
+ import type { GlobalStatus, ParkPhase } from './shared.js';
30
+ import type { TreeWalk } from './tree.js';
31
+ import type { PauseAnswer } from './shared.js';
32
+ /** One session's live park state, as the per-session control sees it. */
33
+ export interface LivePark {
34
+ readonly phase: ParkPhase;
35
+ /** Whether this process can still walk the session to a flushed park. */
36
+ readonly moving: boolean;
37
+ }
38
+ /** What one press of the per-session control did. */
39
+ export type PressResult = 'registered' | 'unrecordable' | 'not-live' | 'not-resumable' | 'already-paused';
40
+ /** What the whole-app control needs from the per-session control. */
41
+ export interface GlobalPauseDeps {
42
+ /**
43
+ * @param sessionId - session to inspect.
44
+ * @returns whether a press already owns this session.
45
+ */
46
+ isPaused(sessionId: SessionId): boolean;
47
+ /**
48
+ * Press one session's pause and make it durable.
49
+ * @param sessionId - session to register.
50
+ * @param signal - cancellation for the resumability and registry reads.
51
+ * @returns what the press did; only `registered` created a new park.
52
+ */
53
+ press(sessionId: SessionId, signal?: AbortSignal): Promise<PressResult>;
54
+ /**
55
+ * Look at one session's mark now that it is paused.
56
+ * @param sessionId - the session that was just pressed.
57
+ */
58
+ look(sessionId: SessionId): void;
59
+ /**
60
+ * @returns every session a live mark owns, with the phase it reports now.
61
+ */
62
+ live(): ReadonlyMap<SessionId, LivePark>;
63
+ /**
64
+ * @returns every registered session this process could not hold again — a
65
+ * record whose session cannot be read here, so nothing in this process can
66
+ * park it, release it, or act on it.
67
+ */
68
+ unheld(): ReadonlySet<SessionId>;
69
+ }
70
+ /** The whole-app park, as the plugin entry drives it. */
71
+ export interface GlobalPause {
72
+ /**
73
+ * Register and park every running session tree in one action. Sessions this
74
+ * pause already owns are left where they are; an idle session is not touched.
75
+ * @param signal - cancellation for the tree reads this action performs.
76
+ * @returns the aggregate as of this action's end.
77
+ */
78
+ pressAll(signal?: AbortSignal): Promise<PauseAnswer>;
79
+ /**
80
+ * The aggregate as structured data for the native Remote stream.
81
+ * @param signal - cancellation for the tree reads this read performs.
82
+ * @returns the aggregate as of this read.
83
+ */
84
+ readState(signal?: AbortSignal, walks?: ReadonlyMap<SessionId, TreeWalk>): Promise<GlobalStatus>;
85
+ /**
86
+ * Report one session a press could not register, so the aggregate names it.
87
+ * The whole-app action records that fact for its own targets; a press of a
88
+ * session's tree reports the members the registry refused through here.
89
+ * @param sessionId - the session a press tried and failed to register.
90
+ * @param unrecorded - whether it is now known to be unparked for that reason.
91
+ */
92
+ markUnrecorded(sessionId: SessionId, unrecorded: boolean): void;
93
+ }
94
+ /**
95
+ * Mount the whole-app park.
96
+ * @param ctx - host context carrying the agent store and the optional subagent
97
+ * and job services.
98
+ * @param watch - the activity fold that says whether a session has unfinished work.
99
+ * @param registry - the durable list this aggregate is a promise about.
100
+ * @param deps - the per-session press and the live phase of every mark.
101
+ * @returns the one-press action and the aggregate read.
102
+ */
103
+ export declare function mountGlobalPause(ctx: Context, watch: ParkWatch, registry: ParkRegistry, deps: GlobalPauseDeps): GlobalPause;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The message history a session's log derives to, and the one rule that reads
3
+ * whether it still owes the model an answer.
4
+ *
5
+ * That question is asked twice and must have one answer: the clean wake asks it
6
+ * before it hands a session an input (an input nobody owes an answer to would
7
+ * open a turn of its own), and the decision list publishes it as the row's
8
+ * `wake` field. A published promise that the wake then declines to keep is the
9
+ * report lying, so both read the same derivation and the same rule.
10
+ *
11
+ * The derivation is the harness's own: the surface fold the loop reads its
12
+ * history through, with the same message projections this process has
13
+ * registered, and an interrupted tail closed exactly as the harness closes it
14
+ * on the next read. `resume.ts` reads the live form of it
15
+ * (`Session.deriveMessages()`); {@link derivedHistory} is the cold form over a
16
+ * stored log, which is all the decision list may claim — it opens a log to read
17
+ * it and never activates a session.
18
+ * @module dsh-pause-session/history
19
+ */
20
+ import type { Message } from '@deepseek-ai/dsh-llm';
21
+ import type { SessionEvent, SessionMessageProjection } from '@deepseek-ai/dsh-session';
22
+ /**
23
+ * Whether one derived history ends owing the model an answer: the last thing
24
+ * said on the model-visible surface is not an assistant message.
25
+ * @param messages - a derived history, in model-visible order.
26
+ * @returns whether a model still owes that history a reply.
27
+ */
28
+ export declare function owesReply(messages: readonly Message[]): boolean;
29
+ /**
30
+ * The messages one stored log derives to, as the harness would read them on the
31
+ * session's next activation: the open tail turn is closed with the harness's
32
+ * own synthetic results first, then the log is folded into its surface.
33
+ * @param events - the stored events, exactly as they are stored and in sequence order.
34
+ * @param projections - this process's registered message projections.
35
+ * @returns the derived history, in model-visible order.
36
+ * @throws when the log cannot be folded — an event this build has no interpreter
37
+ * for, or surface metadata that does not apply to this history.
38
+ */
39
+ export declare function derivedHistory(events: readonly SessionEvent[], projections: readonly SessionMessageProjection[]): Message[];
40
+ /**
41
+ * Whether one stored log names the route a wake would be addressed to. The wake
42
+ * needs a provider and a model to build a request at all, and the request
43
+ * header the session itself persisted is the only place that names them.
44
+ * @param events - the stored events, in sequence order.
45
+ * @returns whether the log's last request header carries both route fields.
46
+ */
47
+ export declare function routeNamed(events: readonly SessionEvent[]): boolean;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Host half of dsh-pause-session.
3
+ *
4
+ * Mounts the pause gate — the per-session hold at the model-request waterfall —
5
+ * the activity fold the park predicate reads, the durable park registry, the
6
+ * session control the browser half drives, and the clean resume path that
7
+ * answers the input a parked session left pending.
8
+ *
9
+ * The registry is read before anything is gated, so a session a previous
10
+ * process left parked is held again by this one without any user action; a
11
+ * registry this build cannot read fails the mount loudly rather than silently
12
+ * resuming every session it named.
13
+ * @module dsh-pause-session
14
+ */
15
+ import { z } from 'zod';
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ export { PauseRemote } from './remote.js';
18
+ /** Plugin name; the Loader row and the bundle patch both use it. */
19
+ export declare const name = "dsh-pause-session";
20
+ /**
21
+ * Required host services: the model runtime to gate, the stores the control
22
+ * needs, the projection registry the park predicate reads turn state from, and
23
+ * the profile's storage, which holds the pause registry.
24
+ */
25
+ export declare const inject: string[];
26
+ /**
27
+ * Provider routes whose request conversion drops an empty user message, so the
28
+ * clean resume path's wake input never reaches the model on them.
29
+ *
30
+ * The list is deployment knowledge, not a constant this plugin may assume:
31
+ * whether an empty user message survives is decided by the adapter (and, for
32
+ * the pi-ai family, by the provider API that adapter is configured for), and a
33
+ * plugin cannot introspect an adapter. The default names the routes the shipped
34
+ * DeepSeek adapter registers; a profile that routes a session elsewhere gets
35
+ * the visible marker instead of a silent empty turn until it declares that
36
+ * route here.
37
+ */
38
+ export declare const DEFAULT_SILENT_WAKE_ROUTES: readonly ["deepseek-official", "deepseek-account"];
39
+ /** Validated plugin configuration. */
40
+ declare const ConfigShape: z.ZodObject<{
41
+ silentWakeRoutes: z.ZodDefault<z.ZodArray<z.ZodString>>;
42
+ maxListedLogEvents: z.ZodDefault<z.ZodNumber>;
43
+ }, z.core.$strip>;
44
+ /** Validated plugin configuration. */
45
+ export type Config = z.infer<typeof ConfigShape>;
46
+ /**
47
+ * The Loader hands a row's config through this schema, and an empty row config
48
+ * arrives as `undefined`; folding it to `{}` first is what lets the field
49
+ * defaults above apply instead of failing the mount.
50
+ */
51
+ export declare const Config: z.ZodPreprocess<z.ZodObject<{
52
+ silentWakeRoutes: z.ZodDefault<z.ZodArray<z.ZodString>>;
53
+ maxListedLogEvents: z.ZodDefault<z.ZodNumber>;
54
+ }, z.core.$strip>>;
55
+ /**
56
+ * Mount the durable pause registry, the pause gate, the activity fold, their
57
+ * session control, and the clean resume path.
58
+ *
59
+ * The registry opens first on purpose: if it cannot be read, nothing else is
60
+ * registered, so a failed mount leaves no half-installed control behind.
61
+ * @param ctx - the plugin's host context.
62
+ * @param config - validated route knowledge for the resume path's wake content.
63
+ */
64
+ export declare function apply(ctx: Context, config?: Config): Promise<void>;