pi-roundtable-mcp 0.7.17 → 0.7.18
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 +11 -0
- package/package.json +2 -2
- package/src/remote-mcp/remote-agent.ts +42 -19
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.18] - 2026-10-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Reserve a remote session atomically so simultaneous dispatches cannot overlap.
|
|
13
|
+
- Keep a timed-out session busy until its underlying answer settles, and release it after synchronous or asynchronous answer failures.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Track active executions by session instead of scanning historical runs to decide whether a session is busy.
|
|
18
|
+
|
|
8
19
|
## [0.7.2] - 2026-10-02
|
|
9
20
|
|
|
10
21
|
The first release from pi-roundtable; 0.7.0 and 0.7.1 were not published for this package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.18",
|
|
4
4
|
"description": "MCP connectors through ContextForge and a remote MCP endpoint for pi-roundtable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@biomejs/biome": "2.5.15",
|
|
47
47
|
"@types/bun": "1.4.2",
|
|
48
|
-
"pi-roundtable": "0.7.
|
|
48
|
+
"pi-roundtable": "0.7.18",
|
|
49
49
|
"typescript": "7.0.2"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -50,6 +50,8 @@ export class RemoteAgent {
|
|
|
50
50
|
readonly #options: RemoteAgentOptions;
|
|
51
51
|
readonly #text: Pick<RemoteMcpMessages, "relayNote" | "runFailed">;
|
|
52
52
|
readonly #runs = new Map<string, Run>();
|
|
53
|
+
/** Actual executions, including calls still settling after a reported timeout. */
|
|
54
|
+
readonly #active = new Map<string, Run>();
|
|
53
55
|
|
|
54
56
|
constructor(options: RemoteAgentOptions) {
|
|
55
57
|
this.#options = options;
|
|
@@ -62,8 +64,16 @@ export class RemoteAgent {
|
|
|
62
64
|
): Promise<{ runId: string; sessionId: string }> {
|
|
63
65
|
this.#prune();
|
|
64
66
|
const session = await this.#session(sessionId);
|
|
67
|
+
// Check and reserve without an await between them: concurrent dispatches may
|
|
68
|
+
// both have finished looking up the same idle session.
|
|
69
|
+
if (this.#active.has(session))
|
|
70
|
+
throw new RemoteAgentError("RUN_IN_PROGRESS");
|
|
65
71
|
const runId = randomUUID();
|
|
66
|
-
const run: Run = {
|
|
72
|
+
const run: Run = {
|
|
73
|
+
sessionId: session,
|
|
74
|
+
state: { status: "working" },
|
|
75
|
+
};
|
|
76
|
+
this.#active.set(session, run);
|
|
67
77
|
this.#runs.set(runId, run);
|
|
68
78
|
void this.#run(run, runId, message);
|
|
69
79
|
return { runId, sessionId: session };
|
|
@@ -75,16 +85,12 @@ export class RemoteAgent {
|
|
|
75
85
|
return run.state;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
/** A new session, or the named one when it exists
|
|
88
|
+
/** A new session, or the named one when it exists. Dispatch reserves it after lookup. */
|
|
79
89
|
async #session(sessionId: string | undefined): Promise<string> {
|
|
80
90
|
const { sessions } = this.#options;
|
|
81
91
|
if (sessionId === undefined) return sessions.create();
|
|
82
92
|
if (!SESSION_ID.test(sessionId) || !(await sessions.touch(sessionId)))
|
|
83
93
|
throw new RemoteAgentError("SESSION_NOT_FOUND");
|
|
84
|
-
const working = [...this.#runs.values()].some(
|
|
85
|
-
(run) => run.sessionId === sessionId && run.state.status === "working",
|
|
86
|
-
);
|
|
87
|
-
if (working) throw new RemoteAgentError("RUN_IN_PROGRESS");
|
|
88
94
|
return sessionId;
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -97,30 +103,47 @@ export class RemoteAgent {
|
|
|
97
103
|
timeoutMs,
|
|
98
104
|
);
|
|
99
105
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const answerResult = Promise.resolve()
|
|
107
|
+
.then(() =>
|
|
108
|
+
answer(
|
|
109
|
+
remoteChannel(run.sessionId),
|
|
110
|
+
`${this.#text.relayNote}\n${message}`,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
.catch((error: unknown): TurnResult => {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
const result = await Promise.race([answerResult, timedOut]);
|
|
121
|
+
if (result.ok) {
|
|
122
|
+
run.state = { status: "completed", text: result.text };
|
|
123
|
+
run.finishedAt = Date.now();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
111
126
|
logger.error(
|
|
112
127
|
{ runId, sessionId: run.sessionId, err: result.error },
|
|
113
128
|
"remote run failed",
|
|
114
129
|
);
|
|
115
130
|
run.state = { status: "failed", error: this.#text.runFailed };
|
|
131
|
+
run.finishedAt = Date.now();
|
|
132
|
+
await answerResult;
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
this.#active.delete(run.sessionId);
|
|
116
136
|
}
|
|
117
|
-
run.finishedAt = Date.now();
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
#prune(): void {
|
|
121
140
|
const keepMs = this.#options.keepMs ?? 60 * 60_000;
|
|
122
141
|
for (const [id, run] of this.#runs) {
|
|
123
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
this.#active.get(run.sessionId) !== run &&
|
|
144
|
+
run.finishedAt !== undefined &&
|
|
145
|
+
Date.now() - run.finishedAt > keepMs
|
|
146
|
+
)
|
|
124
147
|
this.#runs.delete(id);
|
|
125
148
|
}
|
|
126
149
|
}
|