@hyperdrive.bot/paseo-server 0.3.38 → 0.3.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/server/server/agent/providers/claude/agent.d.ts +10 -0
- package/dist/server/server/agent/providers/claude/agent.js +17 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +2 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +1 -0
- package/dist/server/server/agent/providers/claude/transport/pty-query.d.ts +59 -1
- package/dist/server/server/agent/providers/claude/transport/pty-query.js +218 -11
- package/dist/server/server/agent/providers/claude/transport/pty.d.ts +32 -0
- package/dist/server/server/agent/providers/claude/transport/pty.js +69 -5
- package/dist/server/server/agent/providers/claude/transport/sdk.d.ts +2 -0
- package/dist/server/server/agent/providers/claude/transport/sdk.js +4 -0
- package/dist/server/server/agent/providers/claude/transport/types.d.ts +8 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +13 -1
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +27 -3
- package/dist/server/server/session.js +6 -2
- package/dist/server/web-ui/_expo/static/js/web/{index-56443c4ee726ad022938cfa50ba09aee.js → index-73ebfe5c6b82437cad59d50a40d2c8ef.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-56443c4ee726ad022938cfa50ba09aee.js.gz → index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-56443c4ee726ad022938cfa50ba09aee.js.map.br → index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-56443c4ee726ad022938cfa50ba09aee.js.map.gz → index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-56443c4ee726ad022938cfa50ba09aee.js.br +0 -0
|
@@ -49,6 +49,25 @@ function getNodePty() {
|
|
|
49
49
|
export function __setNodePtyForTesting(stub) {
|
|
50
50
|
nodePty = stub;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when something tries to write to a pty whose process has already exited.
|
|
54
|
+
*
|
|
55
|
+
* This has a dedicated type because the two failure modes read identically at the call site
|
|
56
|
+
* and must not: "never spawned" is a programming error, while "already exited" is a normal
|
|
57
|
+
* runtime event that the layer above has to react to by failing the turn. Before this class
|
|
58
|
+
* existed, `doWrite` happily called `_pty.write()` on a dead pty, node-pty swallowed it, and
|
|
59
|
+
* `waitForEcho()`'s fixed timer reported success - so paseo typed prompts into a corpse for
|
|
60
|
+
* fourteen retries and then told the user "the session itself is still running".
|
|
61
|
+
*/
|
|
62
|
+
export class PtyExitedError extends Error {
|
|
63
|
+
constructor(op, exitCode = null, exitSignal = null) {
|
|
64
|
+
super(`PtyTransport.${op} called after the pty process exited`);
|
|
65
|
+
this.code = "PTY_EXITED";
|
|
66
|
+
this.name = "PtyExitedError";
|
|
67
|
+
this.exitCode = exitCode;
|
|
68
|
+
this.exitSignal = exitSignal;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
export class PtyTransport {
|
|
53
72
|
constructor() {
|
|
54
73
|
this._pty = null;
|
|
@@ -57,12 +76,28 @@ export class PtyTransport {
|
|
|
57
76
|
this._rows = DEFAULT_ROWS;
|
|
58
77
|
this._killed = false;
|
|
59
78
|
this._exited = false;
|
|
79
|
+
this._exitInfo = null;
|
|
80
|
+
this.exitHandlers = [];
|
|
60
81
|
this.injectChain = Promise.resolve();
|
|
61
82
|
this.hookHandlers = [];
|
|
62
83
|
this.hookBuffer = [];
|
|
63
84
|
this.bridgeClose = null;
|
|
64
85
|
this.systemPromptFilePath = null;
|
|
65
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Has the child process exited?
|
|
89
|
+
*
|
|
90
|
+
* Public because guarding the write paths fixes the symptom but leaves the question
|
|
91
|
+
* unanswerable: `_exited` was private, so no caller could ask a transport whether its
|
|
92
|
+
* child was alive without provoking an error. `AgentTransport` now declares it.
|
|
93
|
+
*/
|
|
94
|
+
get exited() {
|
|
95
|
+
return this._exited;
|
|
96
|
+
}
|
|
97
|
+
/** Exit status once the child is gone, else null. */
|
|
98
|
+
get exitInfo() {
|
|
99
|
+
return this._exitInfo ? { ...this._exitInfo } : null;
|
|
100
|
+
}
|
|
66
101
|
/**
|
|
67
102
|
* Bridge-side / test entry point — push a hook event into this transport's
|
|
68
103
|
* stream. If no `onHookEvent` handler is registered yet, the event is
|
|
@@ -114,8 +149,24 @@ export class PtyTransport {
|
|
|
114
149
|
cwd: opts.cwd,
|
|
115
150
|
env: opts.env,
|
|
116
151
|
});
|
|
117
|
-
|
|
152
|
+
// ONE internal subscription owns exit state and fans it out to every public onExit()
|
|
153
|
+
// handler. Registering handlers straight onto node-pty (the previous shape) meant a
|
|
154
|
+
// subscriber that attached AFTER the child had already died heard nothing at all -- and
|
|
155
|
+
// a PtyQuery attaches to a long-lived terminal, not only to a fresh boot, so that is a
|
|
156
|
+
// reachable case and not a theoretical one.
|
|
157
|
+
this._pty.onExit(({ exitCode, signal }) => {
|
|
158
|
+
const code = typeof exitCode === "number" ? exitCode : null;
|
|
159
|
+
const sig = signal ?? null;
|
|
118
160
|
this._exited = true;
|
|
161
|
+
this._exitInfo = { code, signal: sig };
|
|
162
|
+
for (const h of this.exitHandlers) {
|
|
163
|
+
try {
|
|
164
|
+
h(code, sig);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// one bad subscriber must not stop the others hearing about the death
|
|
168
|
+
}
|
|
169
|
+
}
|
|
119
170
|
});
|
|
120
171
|
}
|
|
121
172
|
write(text) {
|
|
@@ -138,6 +189,9 @@ export class PtyTransport {
|
|
|
138
189
|
if (!this._pty) {
|
|
139
190
|
throw new Error("PtyTransport.writeRaw called before spawn()");
|
|
140
191
|
}
|
|
192
|
+
if (this._exited) {
|
|
193
|
+
throw new PtyExitedError("writeRaw", this._exitInfo?.code ?? null, this._exitInfo?.signal ?? null);
|
|
194
|
+
}
|
|
141
195
|
this._pty.write(data);
|
|
142
196
|
await this.waitForEcho();
|
|
143
197
|
}
|
|
@@ -148,6 +202,9 @@ export class PtyTransport {
|
|
|
148
202
|
if (!this._pty) {
|
|
149
203
|
throw new Error("PtyTransport.write called before spawn()");
|
|
150
204
|
}
|
|
205
|
+
if (this._exited) {
|
|
206
|
+
throw new PtyExitedError("write", this._exitInfo?.code ?? null, this._exitInfo?.signal ?? null);
|
|
207
|
+
}
|
|
151
208
|
this._pty.write(this.bracketedPaste(text));
|
|
152
209
|
await this.waitForEcho();
|
|
153
210
|
}
|
|
@@ -216,10 +273,17 @@ export class PtyTransport {
|
|
|
216
273
|
if (!this._pty) {
|
|
217
274
|
throw new Error("PtyTransport.onExit called before spawn()");
|
|
218
275
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
276
|
+
this.exitHandlers.push(handler);
|
|
277
|
+
// Already dead: fire, rather than never. Deferred a microtask so a subscriber that
|
|
278
|
+
// registers from a constructor (PtyQuery does) cannot be re-entered before it has
|
|
279
|
+
// finished building itself.
|
|
280
|
+
if (this._exited) {
|
|
281
|
+
const info = this._exitInfo;
|
|
282
|
+
queueMicrotask(() => handler(info?.code ?? null, info?.signal ?? null));
|
|
283
|
+
}
|
|
284
|
+
return () => {
|
|
285
|
+
this.exitHandlers = this.exitHandlers.filter((h) => h !== handler);
|
|
286
|
+
};
|
|
223
287
|
}
|
|
224
288
|
context() {
|
|
225
289
|
if (!this._pty) {
|
|
@@ -53,6 +53,8 @@ export declare class SdkTransport implements AgentTransport {
|
|
|
53
53
|
createQuery(input: ClaudeQueryInput): Query;
|
|
54
54
|
spawn(opts: AgentTransportSpawnOptions): Promise<void>;
|
|
55
55
|
write(text: string): Promise<void>;
|
|
56
|
+
/** True once the SDK query has been closed/returned. Mirrors PtyTransport.exited. */
|
|
57
|
+
get exited(): boolean;
|
|
56
58
|
kill(timeoutMs?: number): Promise<void>;
|
|
57
59
|
onHookEvent(_handler: (event: HookEvent) => void): () => void;
|
|
58
60
|
onData(_handler: (chunk: string) => void): () => void;
|
|
@@ -135,6 +135,10 @@ export class SdkTransport {
|
|
|
135
135
|
const queryWithInput = this._query;
|
|
136
136
|
await queryWithInput.input?.send(message);
|
|
137
137
|
}
|
|
138
|
+
/** True once the SDK query has been closed/returned. Mirrors PtyTransport.exited. */
|
|
139
|
+
get exited() {
|
|
140
|
+
return this._exited;
|
|
141
|
+
}
|
|
138
142
|
async kill(timeoutMs = 5000) {
|
|
139
143
|
if (this._killed)
|
|
140
144
|
return;
|
|
@@ -29,6 +29,14 @@ export type AgentTransportContext = {
|
|
|
29
29
|
cwd: string;
|
|
30
30
|
};
|
|
31
31
|
export interface AgentTransport {
|
|
32
|
+
/**
|
|
33
|
+
* True once the underlying agent process has exited.
|
|
34
|
+
*
|
|
35
|
+
* Optional so a transport that genuinely cannot know stays honest by omitting it;
|
|
36
|
+
* `undefined` means "no liveness signal" and must not be read as "alive". Both shipped
|
|
37
|
+
* transports implement it.
|
|
38
|
+
*/
|
|
39
|
+
readonly exited?: boolean;
|
|
32
40
|
spawn(opts: AgentTransportSpawnOptions): Promise<void>;
|
|
33
41
|
write(text: string): Promise<void>;
|
|
34
42
|
/**
|
package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts
CHANGED
|
@@ -19,9 +19,21 @@ export interface ResolveOrCreateWorkspaceIdInput {
|
|
|
19
19
|
cwd: string;
|
|
20
20
|
initialTitle: string | null;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* `createdWorkspace` is true ONLY when this call minted a brand new workspace
|
|
24
|
+
* record. Callers use it to decide whether post-create side effects that are
|
|
25
|
+
* only correct on a fresh workspace may run — notably workspace auto-naming,
|
|
26
|
+
* which would otherwise rename a workspace the user already had. Deriving that
|
|
27
|
+
* from the REQUEST (`!msg.workspaceId`) is wrong now that an omitted id can
|
|
28
|
+
* resolve to an existing workspace.
|
|
29
|
+
*/
|
|
30
|
+
export interface ResolveOrCreateWorkspaceIdResult {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
createdWorkspace: boolean;
|
|
33
|
+
}
|
|
22
34
|
export interface WorkspaceProvisioningService {
|
|
23
35
|
findOrCreateWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord>;
|
|
24
|
-
resolveOrCreateWorkspaceIdForCreateAgent(input: ResolveOrCreateWorkspaceIdInput): Promise<
|
|
36
|
+
resolveOrCreateWorkspaceIdForCreateAgent(input: ResolveOrCreateWorkspaceIdInput): Promise<ResolveOrCreateWorkspaceIdResult>;
|
|
25
37
|
createWorkspaceForDirectory(cwd: string, title?: string | null): Promise<PersistedWorkspaceRecord>;
|
|
26
38
|
findOrCreateProjectForDirectory(cwd: string): Promise<PersistedProjectRecord>;
|
|
27
39
|
ensureWorkspaceRecordUnarchived(workspace: PersistedWorkspaceRecord): Promise<PersistedWorkspaceRecord>;
|
|
@@ -119,12 +119,36 @@ export function createWorkspaceProvisioningService(deps) {
|
|
|
119
119
|
}
|
|
120
120
|
async function resolveOrCreateWorkspaceIdForCreateAgent(input) {
|
|
121
121
|
if (input.createdWorktree) {
|
|
122
|
-
return
|
|
122
|
+
return {
|
|
123
|
+
workspaceId: input.createdWorktree.workspace.workspaceId,
|
|
124
|
+
createdWorkspace: false,
|
|
125
|
+
};
|
|
123
126
|
}
|
|
124
127
|
if (input.requestedWorkspaceId) {
|
|
125
|
-
return input.requestedWorkspaceId;
|
|
128
|
+
return { workspaceId: input.requestedWorkspaceId, createdWorkspace: false };
|
|
126
129
|
}
|
|
127
|
-
|
|
130
|
+
// Look before you mint. This branch used to call createWorkspaceForDirectory
|
|
131
|
+
// unconditionally, so every createAgent that omitted `workspaceId` produced a
|
|
132
|
+
// SECOND workspace for a directory that already had one — no lookup, not even
|
|
133
|
+
// against the identical cwd. On one daemon that turned 19 directories into 114
|
|
134
|
+
// workspace records, 48 of them on a single repo root, and the agent surfaced
|
|
135
|
+
// in the fresh workspace instead of the one on screen.
|
|
136
|
+
//
|
|
137
|
+
// 0.3.38 fixed the palette by making its caller pass the id. That closes one
|
|
138
|
+
// door and leaves the hole: any other caller (MCP create_agent, loops,
|
|
139
|
+
// schedules, whatever is written next) still mints. Fix it where ownership is
|
|
140
|
+
// actually decided.
|
|
141
|
+
const existing = await findExactWorkspaceByDirectory(input.cwd, { refreshGit: false });
|
|
142
|
+
if (existing) {
|
|
143
|
+
const reused = await reclassifyOrUnarchiveWorkspaceForDirectory({
|
|
144
|
+
workspace: existing,
|
|
145
|
+
project: await projectRegistry.get(existing.projectId),
|
|
146
|
+
cwd: await resolveWorkspaceDirectory(input.cwd, { refreshGit: false }),
|
|
147
|
+
});
|
|
148
|
+
return { workspaceId: reused.workspaceId, createdWorkspace: false };
|
|
149
|
+
}
|
|
150
|
+
const created = await createWorkspaceForDirectory(input.cwd, input.initialTitle);
|
|
151
|
+
return { workspaceId: created.workspaceId, createdWorkspace: true };
|
|
128
152
|
}
|
|
129
153
|
async function createWorkspaceForDirectory(cwd, title) {
|
|
130
154
|
const checkout = await workspaceGitService.getCheckout(cwd);
|
|
@@ -2693,13 +2693,17 @@ export class Session {
|
|
|
2693
2693
|
let createAgentConfig = createdWorktree
|
|
2694
2694
|
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
|
2695
2695
|
: config;
|
|
2696
|
-
const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({
|
|
2696
|
+
const { workspaceId, createdWorkspace } = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({
|
|
2697
2697
|
createdWorktree,
|
|
2698
2698
|
requestedWorkspaceId: msg.workspaceId,
|
|
2699
2699
|
cwd: createAgentConfig.cwd,
|
|
2700
2700
|
initialTitle: workspacePromptTitle,
|
|
2701
2701
|
});
|
|
2702
|
-
|
|
2702
|
+
// Gate auto-naming on what actually happened, NOT on what was requested.
|
|
2703
|
+
// An omitted workspaceId no longer implies a fresh workspace: provisioning
|
|
2704
|
+
// now reuses the one that already owns this cwd, and auto-naming a reused
|
|
2705
|
+
// workspace would rename one the user already had.
|
|
2706
|
+
const createdDirectoryWorkspaceForAgent = !createdWorktree && createdWorkspace;
|
|
2703
2707
|
// Resuming a past session: the recorded cwd may no longer exist on this
|
|
2704
2708
|
// machine (different $HOME, deleted folder). The transcript is read from
|
|
2705
2709
|
// ~/.claude/projects regardless, so fall back to an existing directory
|