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.
- package/CHANGELOG.md +41 -0
- package/DEVELOPMENT.md +55 -0
- package/LICENSE +21 -0
- package/README.md +130 -2
- package/README.zh.md +91 -0
- package/REHEARSAL.md +90 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/pause-controls.webp +0 -0
- package/icon.svg +5 -0
- package/lib/client.js +2299 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +2975 -0
- package/lib/index.js.map +1 -0
- package/lib/types/client/burst.d.ts +253 -0
- package/lib/types/client/control.d.ts +232 -0
- package/lib/types/client/global.d.ts +325 -0
- package/lib/types/client/icons.d.ts +24 -0
- package/lib/types/client/index.d.ts +77 -0
- package/lib/types/client/locales.d.ts +69 -0
- package/lib/types/client/pill.d.ts +49 -0
- package/lib/types/client/view.d.ts +137 -0
- package/lib/types/coldlog.d.ts +111 -0
- package/lib/types/composition.d.ts +94 -0
- package/lib/types/control.d.ts +114 -0
- package/lib/types/gate.d.ts +110 -0
- package/lib/types/global.d.ts +101 -0
- package/lib/types/history.d.ts +47 -0
- package/lib/types/index.d.ts +64 -0
- package/lib/types/park.d.ts +50 -0
- package/lib/types/registry.d.ts +117 -0
- package/lib/types/resume.d.ts +221 -0
- package/lib/types/shared.d.ts +324 -0
- package/lib/types/tree.d.ts +71 -0
- package/lib/types/triage.d.ts +121 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +148 -4
|
@@ -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;
|